#250 Authentication from Scratch (revised)
- Download:
- source codeProject Files in Zip (102 KB)
- mp4Full Size H.264 Video (25 MB)
- m4vSmaller H.264 Video (14.4 MB)
- webmFull Size VP8 Video (17.5 MB)
- ogvFull Size Theora Video (30.7 MB)
User authentication is required in almost every Rails application, including the blogging application shown below. This app currently has no authentication at all and we want to add some so that users can sign up and then log in to and out of the site.
There are several gems that will help us to add authentication to an app, such as Devise and OmniAuth. Sometimes though we just need simple password-based authentication and in these cases it’s not difficult to create it from scratch. We’ll do just that in this episode.
Adding a Signup Form
The first thing we need to do is to add a sign-up form for creating a new user and then add a link to it. This means that we’ll need to generate a User
model and a controller to handle the signup process. We’ll use the resource generator to do this. This is similar to the scaffold generator but it doesn’t fill in all the controller actions. We’ll give the User
model email
and password_digest
fields.
$ rails g resource user email password_digest
Having a password_digest
field is important as it’s the default name that’s used with Rails’ has_secure_password
feature and we’ll be using this feature later. This command will generate a User
model, a UsersController
and a database migration. We’ll migrate the database before we continue.
$ rake db:migrate
Next we’ll add has_secure_password
to the User model. This was introduced in Rails 3.1 and adds some simple authentication support to the model using that password_digest
column.
class User < ActiveRecord::Base has_secure_password end
To get this working in Rails we’ll also need to modify the gemfile and uncomment the line that includes the bcrypt-ruby
gem as this gem handles hashing the password before its stored in the database.
# To use ActiveModel has_secure_password gem 'bcrypt-ruby', '~> 3.0.0'
We need to run bundle
to ensure that this gem is installed and to restart the server for the changes to be picked up.
Another important change to make in the User
model is to call attr_accessible
and specify the attributes that can be set by mass assignment and through the form. We’ll set email
, password
and password_confirmation
but you can set whatever fields you want to match your signup form.
class User < ActiveRecord::Base has_secure_password attr_accessible :email, :password, :password_confirmation validates_uniqueness_of :email end
Having this in place means that if we add an admin
column to the database it won’t be possible for a malicious user to make themselves an admin user by sending a request including that value to the UsersController
’s update
action. We’ve also added a validation to the model to ensure that the email address is unique and we could add other validations here to validate the format of the email, the length of the password and so on. We don’t need to validate the presence of the password
and password_confirmation
fields, however, as this will be handled by has_secure_password
.
Our User
model is looking good so let’s move on to creating the signup form. We’ll do this inside the UsersController
that we generated earlier. This is blank by default so we’ll need to add a couple of actions to handle creating new users. The code for these is fairly standard.
class UsersController < ApplicationController def new @user = User.new end def create @user = User.new(params[:user]) if @user.save redirect_to root_url, notice: "Thank you for signing up!" else render "new" end end end
The new
action will display the signup form and the create
action is called when the form is submitted. This will validate the values from the form and if this passes it will redirect the browser back to the home page and show a message. If the validation fails the signup form will be redisplayed.
The signup form is shown below. Again, this is fairly standard. It uses form_for
with the @user
instance variable, has a section for displaying any errors and finally three fields for email
, password
and password_confirmation
.
<h1>Sign Up</h1> <%= form_for @user do |f| %> <% if @user.errors.any? %> <div class="error_messages"> <h2>Form is invalid</h2> <ul> <% @user.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :email %><br /> <%= f.text_field :email %> </div> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation %> </div> <div class="actions"><%= f.submit "Sign Up" %></div> <% end %>
Now all we need is a link to this form. We’ll put it in the application’s layout file.
<div id="user_header"> <%= link_to "Sign Up", new_user_path %> </div>
When we reload the home page now the “Sign Up” link is visible and clicking it will take us to our new form.
If we fill in this form incorrectly we’ll see validation errors as we’d expect. Once we’ve filled it in successfully we’ll be redirected back to the home page and we’ll see a flash message that tells us we’ve signed up.
Logging In And Out
So far we haven’t done any actual authentication, we’ve just created a User
record. For authentication we need a login form. We’ll create that now and also add a “Log In” link next to the “Sign Up” one. First we’ll create a new SessionsController
to handle logging in and give it a new
action.
$ rails g controller sessions new
We’ll need to adjust the routes that were generated by this command. The SessionsController
is a RESTful-style controller so we’ll delete the generated get "sessions/new"
route and add a new sessions
resource.
Blog::Application.routes.draw do resources :sessions resources :users root to: 'articles#index' resources :articles end
We can leave the new action blank in the controller but we will modify the associated view as this is where the login form will go.
<h1>Log In</h1> <%= form_tag sessions_path do %> <div class="field"> <%= label_tag :email %><br /> <%= text_field_tag :email, params[:email] %> </div> <div class="field"> <%= label_tag :password %><br /> <%= password_field_tag :password %> </div> <div class="actions"><%= submit_tag "Log In" %> <% end %>
As our SessionsController
doesn’t have a model behind it we use form_tag
for this form. It POSTs to the sessions_path
which will trigger the SessionsController
’s create
action and we’ll write that action next.
def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) session[:user_id] = user.id redirect_to root_url, notice: "Logged in!" else flash.now.alert = "Email or password is invalid." end end
When the form is submitted we try to find a user by the email address that was entered on the form. If we find one we use authenticate
to check that the entered password is correct. This is a method that has_secure_password
provides and will return a boolean value. If the password matches we store the user’s id
in a session variable and redirect to the home page. If either the username or password are incorrect the form is shown again with an error message. We use flash.now
here as we need the message to be displayed immediately, not after a redirect.
To finish this feature we just need to add a link next to the “Sign Up” link.
<div id="user_header"> <%= link_to "Sign Up", new_user_path %> or <%= link_to "Log In", new_session_path %> </div>
If we reload the home page now we’ll see the new “Log In” link and if we click it we’ll be taken to our new form. If we use the email address and password we entered when we signed up we’ll be able to log in to the site.
Even though we’ve logged into the site now the “Log In” link still shows. It would be better if this changed to show that the user is currently logged in. To do this we need a way to fetch the currently logged-in user record and we’ll do this in the ApplicationController
so that it’s available in all controllers.
class ApplicationController < ActionController::Base protect_from_forgery private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end helper_method :current_user end
This current user will look for a User
based on the session’s user id
that was stored when they logged in, if that session variable exists. This method will probably be called many times per request and so it’s a good idea to cache it in an instance variable as we have done here. We’re going to need to access this method in the views, too, so we’ve used helper_method
to make it a helper method. We can use this now to change the “Sign Up” and “Log In” links when the user is logged in and display a message telling the user that they’re logged in and showing their email address.
<div id="user_header"> <% if current_user %> Logged in as <%= current_user.email %>. <% else %> <%= link_to "Sign Up", new_user_path %> or <%= link_to "Log In", new_session_path %> <% end %> </div>
As we’re logged in reloading the page now will display the email address we signed up with. If your authentication setup uses usernames instead you could display that here just as easily.
Logging Out
While we can log in now there’s no way to log out. We need a new link that shows when we’re signed in and we’ll need a new action in the SessionsController
to handle that behaviour.
def destroy session[:user_id] = nil redirect_to root_url, notice: "Logged out!" end
This action is pretty simple. We just clear out the user_id
session variable and redirect back to the home page. We could clear the entire session by calling reset_session
instead, but clearing the user_id
is enough to sign the user out. Now in the layout file we can add a “Log Out” link. Pointing it to the destroy
action can be a little bit tricky because the session_path
helper method expects an id
to be passed in. We’ll just pass “current” in but we could pass in anything as the controller doesn’t read this parameter. We also need to set the method
to delete
so that the destroy action is triggered.
<div id="user_header"> <% if current_user %> Logged in as <%= current_user.email %>. <%= link_to "Log Out", session_path("current"), method: "delete" %> <% else %> <%= link_to "Sign Up", new_user_path %> or <%= link_to "Log In", new_session_path %> <% end %> </div>
Reloading the page will now show us the “Log Out” link and when we click it we’ll be logged out.
Better Routes
We could make the “Sign Up” and “Log In” URLs a little prettier. The login form is currently at /sessions/new
but it would better to have /login
point to this. We can add a three custom routes to improve the URLs for signing in and logging in and out.
Blog::Application.routes.draw do get 'signup', to: 'users#new', as: 'signup' get 'login', to: 'sessions#new', as: 'login' get 'logout', to: 'sessions#destroy', as: 'logout' resources :sessions resources :users root to: 'articles#index' resources :articles end
One thing to note about the logout
path is that we’re using get
. We might consider using delete
instead as technically this action does change user state but as it doesn’t do any permanent damage we’ll leave this as get
here. Now we can update our layout file to use these new routes.
<div id="container"> <div id="user_header"> <% if current_user %> Logged in as <%= current_user.email %>. <%= link_to "Log Out", logout_path %> <% else %> <%= link_to "Sign Up", signup_path %> or <%= link_to "Log In", login_path %> <% end %> </div>
Automatically Logging In New Users
One loose end that we’ve left untied is that when the user signs up they aren’t automatically logged in. The user record is created but the new user has to enter their details again to sign in. This is easy to fix by modifying the UsersController
’s create
action and setting the user_id
session variable when the new user is saved.
def create @user = User.new(params[:user]) if @user.save session[:user_id] = @user.id redirect_to root_url, notice: "Thank you for signing up!" else render "new" end end
If we find that there’s duplication in the logic between the SessionsController
’s logging in behaviour and this behaviour we can abstract this out into some kind of controller method which can be shared between them. Here it’s OK to have this small amount of duplication. If we sign up as a new user now we’ll be signed in straight away.
Basic Authorization
A common requirement when dealing with authentication is to make sure that they’re logged in before giving them access to a specific page. Our application shows a number of articles and each article’s page has an “Edit” link which takes us to a page where we can edit that article. We want our application to only allow logged-in users to edit articles. Since this is such common behaviour we’ll define it in the ApplicationController
so that we can use it anywhere.
def authorize redirect_to login_url, alert: "Not authorized" if current_user.nil? end
This method will check for a current user and redirect to the login page if it fails to find one with an alert telling them that they aren’t authorized. We can now use this method as a before_filter
on any controller action we want to protect, such as the ArticlesController
’s edit
and update
actions.
class ArticlesController < ApplicationController before_filter :authorize, only: [:edit, :update] def index @articles = Article.all end def show @article = Article.find(params[:id]) end def edit @article = Article.find(params[:id]) end def update @article = Article.find(params[:id]) if @article.update_attributes(params[:article]) redirect_to @article, notice: "Article has been updated." else render "edit" end end end
We can still edit an article now when we’re logged in but if we log out and try again we’ll be redirected.
You could improve this user experience depending on how your application works, but the basic functionality is here. If you have more complex authorization logic than this you could use a gem like CanCan to help with this.
Now we have a complete authentication solution built from scratch and there are a variety of ways we could add on to this. For example in UsersController we might want a profile page or a way to update a user’s information. We could do this easily by adding show or edit actions to this controller. If we want to store the user’s login in a permanent cookie instead of a temporary session take a look at episode 274 which shows how to add “Remember Me” functionality and also a way for users to reset their password.