Skip to main content

54 User Authentication with Devise

Episode 12 · July 7, 2014

A look into user accounts, registration, login, forgot your password, and more

Gems Authentication


Transcripts

In this episode, we're going to talk about using the devise gem to add user registration, log-in, log-out and other features to our bookstore application.

The devise gem is one of the most popular gems that's out there, there's 2,000 forks of people working on it themselves, and the commits to master in the gem are as recent as three days ago. It's really actively being developed, I've written some code for it in the past and absolutely enjoy working with it.

We're going to talk about how we can add this into our bookstore application, so that only registered users can add a new book, we don't want people coming to the site and spamming it with new books, for example.

We're going to add devise so that we can track who created the book. That is what we're going to do in this episode and let's begin!

As always, the first thing we need to do is hop over to RubyGems and grab the line from the gemfile. I just wanted to point out here that there have been seven million total downloads for devise which is pretty impressive and kind of shows you exactly what I was talking about with how popular this gem is.

As usual, we're going to open up our Gemfile, jump into the bottom, paste in devise, run bundle (in the terminal), restart our rails server and we can run a couple commands to install devise and set it all up.

The first thing we need to do is run

rails generate devise:install

This command is going to install a config file in the initializers folder and then it will also install some messages that you can customize for the notices that you see in the application.

Now, once this is done, it makes some messages here such as the config action mailer options and this is for when it sends out an email and you want to make sure that they put in the right host name in the URL. So when you're in development they recommmend... If you're using localhost:3000, to go and operate with that URL.

And then they recommend having a route in your Rails application and putting in notices and alerts so that you can see what's going on.

If you also want to customize the views you can run

rails g devise:views

and this will install the views, the html.erb templates for all the different devise screens that you can have. I recommend doing that because I almost always customize it and add bootstrap-html around the form fields so that it looks pretty. And then lastly, we need to run

rails generate devise User

This is going to generate a User model for us, and it will populate it with an email address and a password, and a bunch of other automatically tracked options, such as the last time they signed in, the last IP address they signed in, and a bunch of things like that, and we're just going to use the defaults and generate this. Now that we have that we can run

rake db:migrate

to create our users, and if we refresh our application after generating on the devise stuff, you will get an undefined method 'devise' for User When you create your users with devise it adds a line in there that says devise and the options that it's going to run:

app/models/user.rb

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable, :recoverable, :reemberable, :trackable, :validatable
 end

Now, if you get this error, you just need to restart your rails application again and that will fix it because if you have installed the devise initializer and that hasn't been reloaded you need to make sure that that gets reloaded first, so that that is available.

So our application doesn't look any different we now have users, but we haven't actually done anything with them so that is going to be our next step. We're going to add a signup and login buttons to the top right here and then when you're signed in we'll change those to your account page and a log out page.

To add those links to the top right corner we need to open up our nav bar partial that we created before

app/views/shared/_navbar.html.erb

and if you look at the bottom here, there is a line that says navbar-right as the class. This is going to be the links on the right side of the nav bar. We can see there's the link and then this is all the html for the dropdown that we have.

We're not going to use the drop down, so I'm just going to delete that and we're going to take this link and replace that with a “Sign Up” link.

Now the link_to that we want to create is going to be called “Sign Up” and we're going to send it to the new_user_registration_path

Now this is a URL path that has been generated by devise, when you installed devise, the routes file added a line called devise_for :users. This is something that devise generates and automatically sets up, but it's similar to the resources routes in that it adds a bunch of URL's, so it adds a registration path, forgot your pasword (path), it adds log in (path), log out (path) and a bunch of things.

If you go into your terminal and run

rake routes | grep user

You can see all of the routes that devise generates, and they will be the ones that the controller starts with the name devise

We're going to use the new_user_registration_path, we're going to add the new_user_sessions_path for log in, and we will also add the destroy_user_session_path to log out.

You can play with all of these, they're all available, you don't necessarily need to link to some of these because they're sort of internal; when you log out it handles it or when you create your registration.

You can play with those and learn more about that but inside our application we want to add in the log in method here and this is the new_user_session_path.

And if we save that and reload our application, we can now see there's a sign up and login link here. We go to sign in or sign up, we can have our email address and password form and our sign in is the same set, same thing, but without a password confirmation. This is what devise generates and these views are located inside the gem itself, so when we come into our terminal and run

rails generate devise:views

This will copy all of those views into our application and we can go customize them. Let's customize our sign up form:

app/views/devise/registration/new.html.erb

and see exactly what we're looking at. This has a bunch of stuff that they automatically generate, it's pretty close to bootstrap, so if we add a

 <div class="form-group"></div>

around the form, we get this new form. We've added the form control class to each of these options and I've done the same with the Sign In in the edit_registration_path. Let's take a look at this:

If I register with my email address and password and sign up, we get taken to the homepage, it looks like everything may have worked, but I'm not really sure, we didn't get any message and these links at the top didn't change either, so one of the devise things that I mentioned when we installed it was to make sure to put the notice and the alerts in your app/views/layouts/application.html.erb, so let's do that

Bootstrap provides a class called alert and we can add that both for the notice and the alert, so I’m just going to put

 <div class="alert alert-info"><%= notice %></div>
 <div class="alert alert-warning"><%= alert %></div>

We can put those in here, and they will show up every single time, because we need to check if there's a message of not before we display it. However, you can see immediately in the notice that we signed up successfully. This is a message that devise provides and it only shows up one time so if we refresh the page it goes away, and because we don't want those alerts to show up if the message is empty, let's add an if statement here and only display the notice when there is one and the alert the same way.

If we save this and come back to our browser, now those only show up when they're available.

If we go back to our nav bar, we can take a look at this code here because we added the links, and we're signed in now, but we're actually not saying a different UI, it doesn't seem any different, and we need to make it so you can Log out as well.

Now devise provides a method called user_signed_in?. This is a method that devise uses to provide you a helper to check to see if the user is signed in or not. And what it does is it looks at the cookie session and determines if there's a user stored in there as currently logged in, so if you were to clear your cookies you would become logged out.

When the user is signed in, we want to have a separate menu; otherwise we will display the Sign Up and Login links. When you are logged in, we'd like to have a list item here, with a link to the “Log Out”, and this is going to go to the destroy_user_session_path, and this one is a bit special because we need to have method: :delete at the end of it. And the reason why we need to do this is because devise requires you to make a delete HTTP request to Log Out and the reason why they do that is because it needs to come from you. You don't want to click on a malicious link from another website that logs you out because that is unintentional and it's not really a good thing.

(This last bit of code looks like this:)

app/views/shared/_navbar.html.erb

<% if user_signed_in? %>
 <li><%= link_to "Logout", destroy_user_session_path, method: :delete %></li>
<% else %>
 <li><%= link_to "Sign Up", new_user_registration_path %></li>
 <li><%= link_to "Login", new_user_session_path %></li>
<% end %>

The AJAX call will take when you click on this link the jQuery UJS that comes by default with Rails and you can see that in the application.js it requires jQuery UJS which looks for those methods or links that say: method: :delete or method: :post or method: :put and that will intercept it and it will initiate an AJAX request to make a delete link and submit that across so now if we save this and refresh our browser we can see that we have a "Sign Out" or "Log Out" link. If we click that, we get signed out successfully and we can refresh the page and that message goes away. We can log back in and see that everything is properly working.

The last thing I would like to add here is a link to your user account, and we're going to display your email address that you're signed in as here and we can do that by accessing current_user.email. This is going to link to the edit_user_registration_path. When you use devise and a user is signed in you have access to a method called current_user which returns a user object and that happens to be the one that the user is signed in as, if you add anything to your user model in here, you add associations or anything like that, it's going to be directly accessible off of the current user so you'll be able to talk to all of those books that I might own as a user and so on, and one way to do that for the email address is to grab it off the user, so we can refresh the page now and see that now my email address is displayed and if I click on it I get to devise's user registration form which I've styled some of it

(This last bit of code looks like this:)

app/views/shared/_navbar.html.erb

<% if user_signed_in? %>
 <li><%= link_to current_user.email, edit_user_registration_path %></li>
 <li><%= link_to "Logout", destroy_user_session_path, method: :delete %></li>
<% else %>
 <li><%= link_to "Sign Up", new_user_registration_path %></li>
 <li><%= link_to "Login", new_user_session_path %></li>
<% end %>

This is your account page and you can update your password and change your email address or cancel your account, this comes by default with devise and you're able to go through everything pretty simply like you don't have to do too much work and have all this functionality already built for you which is great.

Of course, having user accounts, doesn't benefit anyone if you're not really using them; so what we're going to do next is we're going to take the new book and create methods and we're going to make it so that only registered users can add a book in our system and we're also going to take the books and record who created it so that we can keep track of that information.

Inside our books controller we can add

app/controllers/books_controller.rb

before_action :authenticate_user!

and this is going to call a devise method called authenticate user every time one of these actions inside the books controllers is accessed and what that's going to simply do is it's going to check the same thing as user signed in. We want to make sure that a user is signed in, and if they're not, it redirects them to the sign in or sign up page and we can test this by logging out, and when it comes back to the homepage when you log out it immediately takes you to the sign in page. That seems kind of weird, however, that's because our index and show actions are also being protected by this, so if we way we want to protect everything except the index and the show actions, we can go back into our application and go to the home screen now, and if we click into a book, everything is fine, but if we click on edit, we're asked to sign in and that is one of the simple ways that you can start adding some security to your application by requiring a user to be signed in for any for any of these actions, and you want to make sure that this is:

before_action :authenticate_user!, except: [:index, :home]

is a whitelist of actions that are allowed for public users, so you want to protect everything by default, and essentially poke holes through that so that the public users or the people who aren't signed in can actually do certain things, with that change we've made it so that if you click on adding a new book when you're signed out, it will make sure that you're signed in when you do that, so this requires users to be signed in and we can simply sign in and continue with adding the new book, so we've set it up properly so that everything is protected now we kind of want to automatically add the user to the book as we create the book so then we want to record who created the book so we can kind of keep details on that, see who our power users are, for example.

The way we're going to accomplish that is by generating a migration:

rails g migration AddUserIdToBooks user_id:integer

and

rake db:migrate

Our next step is to add the associations into the user and book models and we can start in the

app/models/user.rb

has_many :books 

and

app/models/book.rb

belongs_to :user  

This allows us to say that a user has many books, so user.books is a method that we have available now.

And inside of our controller for books, our create action can now instead of being a global Book.new, we can scope it to the current user, (by modifying that line:)

@book = current.user.books.new(book_params)

and this will automatically assign the user ID into the new book, so this is really cool and we can automatically assign the book and we know who was the user that created it, so we know who de owner is.

So to wrap up with devise, we've created a user model that we've configured our rails application to take advantage of, we've added registration form and edit registration form, added the session, login, log out, we've also configured our books model to integrate with that user who's currently signed in, so this is something that will bleed into all aspects of your application as you move forward and with devise you have a very simple way of configuring it so there's a ton of benefits to using devise and I highly encourage you to dive deeper into it.

Transcript written by Miguel

Discussion


Gravatar

If like me you need to add some fields in User model. You can simply create your own registration controller and define sign_up_params and account_update_params to overload the devise controller.

I don't know if that is the best practice... but it works.

Source: http://www.jacopretorius.ne...

Enjoy ;)

Gravatar

Thanks for sharing Maxime! I've also written a post on this about without having to setup a new controller. https://gorails.com/blog/ra...

Gravatar

Indeed, your approach is easier to implement I think ;)
I'm going to read all your posts ASAP!


Gravatar
Im getting a routing error when i click logout...

Login or create an account to join the conversation.