Nov 11

Date published: 11-11-2013

Rails version: 4.0.0

User authentication

Almost every web site these days requires users to log in before accessing content on the site, so it’s important for web developers to understand user authentication. Rails has several open source options for user authentication, but the most popular and one of the most versatile is called Devise. In this tutorial, I’ll show how to install Devise and setup basic authentication to allow the user to log in with their email address. Once we understand how Devise works, we’ll expand it so that with only one field users can log in with either their username or email address.

Sign up screen

Sign up screen

Copy the template site

The first thing we need to do is copy our template site and create a new project from it. Open a terminal window and switch to the directory where the template Bootstrap site is located. Run the following commands:

$ cp -r template_bootstrap_site/ template_devise_site
$ cd template_devise_site/

It’s a good idea now to change the module name just like we did in this tutorial in the section titled, “Update the module information.” Make sure to commit your changes to Git before moving on.

Adding the Devise gem

Open the Gemfile and add the following line:

gem 'devise', '3.0.0'

There’s quite a bit of setup to get the gem working, so switch over to the console and run the following commands:

$ bundle install
Resolving dependencies...
…
Your bundle is complete!
$ rails generate devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
=========================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { :host => 'localhost:3000' }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root :to => "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. If you are deploying on Heroku with Rails 3.2 only, you may want to set:

       config.assets.initialize_on_precompile = false

     On config/application.rb forcing your application to not access the DB
     or load models when precompiling your assets.

  5. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

=========================================

You’ll notice that Devise has printed out some instructions. For now the only ones we’re going to worry about are the first and last ones. Open config/environments/development.rb and add the following:

  # Setup for Devise
  config.action_mailer.default_url_options = { :host => 'localhost:3000' }

Go back to the console and run the following command:

$ rails g devise:views
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
  ...

Now if you look in the app/views/devise folder, you’ll see that all the views that Devise shows to the user have been generated and can be modified. Now we need to generate our User model so that information can be stored in the database. Type the following into the console:

$ rails generate devise User
      invoke  active_record
      create    db/migrate/20131108213834_devise_create_users.rb
      create    app/models/user.rb
       route  devise_for :users
$ rake db:migrate
==  DeviseCreateUsers: migrating ==============================================
-- create_table(:users)
   -> 0.0059s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0009s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0006s
==  DeviseCreateUsers: migrated (0.0078s) =====================================

Devise creates the user model for us at app/models/user.rb. Open that file and look at what’s been created for us. We’re going to make one small change to this file because we don’t want to worry about email confirmations at the moment. We’re going to move the :recoverable keyword up to the list of commented out functionality, and now user.rb should look like:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable, :recoverable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :rememberable, :trackable, :validatable
end

Devise also modified config/routes.rb file by adding a call to devise_for at the top. This will automatically generate routes for us to use to access user authentication. Open the console and run the following command to see what routes are available:

$ rake routes
                  Prefix Verb   URI Pattern               Controller#Action
        new_user_session GET    /users/sign_in(.:format)  devise/sessions#new
            user_session POST   /users/sign_in(.:format)  devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
cancel_user_registration GET    /users/cancel(.:format)   devise/registrations#cancel
       user_registration POST   /users(.:format)          devise/registrations#create
   new_user_registration GET    /users/sign_up(.:format)  devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)     devise/registrations#edit
                         PATCH  /users(.:format)          devise/registrations#update
                         PUT    /users(.:format)          devise/registrations#update
                         DELETE /users(.:format)          devise/registrations#destroy
                    root GET    /                         static_pages#home
                   about GET    /about(.:format)          static_pages#about

Note: the step I’m about to describe is an optional change just to explain how to do it. What if we don’t like the terminology that Devise uses to name its actions? We can modify it by changing the call to devise_for. Open config/routes.rb and change it to look like this:

  devise_for :users, :path => "auth", :path_names => {
    :sign_in => 'login', :sign_out => 'logout', :sign_up => 'register' }

Now if you run rake routes in the console, you’ll see the paths have all changed. For the tutorial, we’re going to revert to use the defaults provided by Devise. If you’d rather use different terminology, just make sure to keep in mind you may have to change a few things in the rest of the tutorial if you encounter any problems with the naming standards you’re using.

Let’s update our header to add convenient links for sign up, sign in, and sign out. Open app/views/layouts/_header.html.erb and make the following changes:

        <%= link_to "User Authentication", root_path, class: "brand" %>
        <div class="nav-collapse collapse">
          <ul class="nav">
            <li><%= link_to "About", about_path %></li>
          </ul>
          <ul class="nav pull-right">
            <% if user_signed_in? %>
              <li><%= link_to "Settings", edit_user_registration_path %></li>
              <li><%= link_to "Sign out", destroy_user_session_path, :method => :delete %></li>
            <% else %>
              <li><%= link_to "Sign up", new_user_registration_path %></li>
              <li><%= link_to "Sign in", new_user_session_path %></li>
            <% end %>
          </ul>
        </div>

Notice the line where we call user_signed_in? to see if the user is currently signed in. This is a helper method provided by Devise so we know when the user is signed in, and we use it to show the correct links to either sign in or out. You should be able to create users, edit their settings, sign in, and sign out using the views generated by Devise. Since basic user authentication is working, let’s commit our changes to Git before moving on. In the console window, run the following commands:

$ git add .
$ git commit -m "Added Devise for user authentication"

Add username to the model

Before we make any changes to our code, let’s create a new branch in Git. This way if we use this project as a template for other projects, we can decide if we want just basic sign in with only the email address or a more robust user model with a username we can use to sign in. Open the console window and type the following command:

$ git checkout -b username
Switched to a new branch 'username'

This created a brand new branch called username and automatically switched to use that branch. Now any changes we make and commit will be to the username branch, and if we decide later we don’t want usernames, we can always switch back to that branch and use it.

The next thing we want to be able to do is setup Devise so that on our sign in screen the user can type in either their email address or a username to sign in. First we need to create a username field in the User table, but there’s one caveat we need to address. If you’ve already created users in the User table, they won’t have a username if we add the field and run a migration. If you were going to update a real application, you’d need to worry about how to assign a username to all the current users in the database. Since this is just a tutorial, we can reset the database and start from scratch. Switch to the console window and run the following commands:

$ rake db:reset
-- create_table("users", {:force=>true})
   -> 0.0869s
-- add_index("users", ["email"], {:name=>"index_users_on_email", :unique=>true})
   -> 0.0049s
-- add_index("users", ["reset_password_token"], {:name=>"index_users_on_reset_password_token", :unique=>true})
   -> 0.0046s
-- initialize_schema_migrations_table()
   -> 0.0089s
$ rails generate migration add_username_to_users username:string:uniq
      invoke  active_record
      create    db/migrate/20131111201915_add_username_to_users.rb
$ rake db:migrate
==  AddUsernameToUsers: migrating =============================================
-- add_column(:users, :username, :string)
   -> 0.0050s
-- add_index(:users, :username, {:unique=>true})
   -> 0.0011s
==  AddUsernameToUsers: migrated (0.0062s) ====================================
$ annotate
Annotated (1): User

Because we added the username field, we should add some validation to make sure we get real values. We want our usernames to be unique and be at least 3 characters but no more than 12 characters long. Add the lines for validation below to app/models/user.rb:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable, :recoverable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :rememberable, :trackable, :validatable

  validates :username, presence: true, uniqueness: true,
    length: { minimum: 3, maximum: 12 }
end

The next thing we need to do is update the controller to tell it that we’re going to be passing a username in when we create new users. Rails 4 uses strong parameters to control what can be updated in the model, and this is normally handled in the controller. We can’t see the Devise controller, so how do we make it that the controller will allow us to pass a username from our views? We need to modify app/controllers/application_controller.rb to look like this:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  before_filter :configure_permitted_parameters, if: :devise_controller?

  protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:username, :email, :password, :password_confirmation) }
      devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:email, :password, :password_confirmation, :current_password) }
    end
end

So what did we just do? We told the application that before doing any action in devise_controller, run our method to configure strong parameters. We gave it a list of parameters to use for the sign_up action, and if we add any other parameters to user in the future, we’ll have to update this list of parameters. We’ll be coming back to this file shortly to add a call for the sign_in action, but for now let’s make sure our users can sign up successfully. We need to add a field on the sign up page so the user can specify their username. Open app/views/devise/registrations/new.html.erb and make the following changes:

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  <div><%= f.label :username %><br />
  <%= f.text_field :username, :autofocus => true %></div>

  <div><%= f.label :email %><br />
  <%= f.email_field :email %></div>

Make sure to remove the call to autofocus from the email field as it’s no longer the first field on the page. Start the server and try creating a new user on the sign up page.

Change how we sign in

The only thing left to do is change our sign in page so that the user can sign in with either the email address or username of the account. Let’s make a few changes in the User model first. Update app/models/user.rb:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable, :recoverable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :rememberable, :trackable, :validatable,
         :authentication_keys => [:login]

  validates :username, presence: true, uniqueness: true,
    length: { minimum: 3, maximum: 12 }

  attr_accessor :login

  def self.find_first_by_auth_conditions(warden_conditions)
    conditions = warden_conditions.dup
    if login = conditions.delete(:login)
      where(conditions).where(["username = :value OR email = :value", { :value => login }]).first
    else
      where(conditions).first
    end
  end
end

We added an accessor for a variable called login and defined a method to handle authentication of what gets passed to us from the login page. Also, note that we added an array called authentication_keys to our call to devise so that it knows to login using that. Remember when we updated the strong parameters for sign_up I said we’d need to update them for sign_in also? Let’s do that now by adding a line for sign_in in app/controllers/application_controller.rb:

    def configure_permitted_parameters
      devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:username, :email, :password, :password_confirmation) }
      devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:email, :password, :password_confirmation, :current_password) }
      devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :password, :remember_me) }
    end

Now we can change our sign in page to use the new login parameter instead of email. We’re going to remove the email field and replace it with a login field in app/views/devise/sessions/new.html.erb:

<%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %>
  <div><%= f.label :login %><br />
  <%= f.text_field :login, :autofocus => true %></div>

  <div><%= f.label :password %><br />
  <%= f.password_field :password %></div>

There’s one more small change we need to make so that users get correct error messages when trying to sign in. Open app/config/locales/devise.en.yml and change these two lines to say login instead of email:

  en:
    failure:
      invalid: "Invalid login or password."
      not_found_in_database: "Invalid login or password."

That’s all we need to do to get it working. Restart the Rails server and try out creating new accounts, signing in, and signing out. You should be able to sign in with either the username or email address in the same field. Since we’re done, let’s commit our changes in Git so we don’t lose them:

$ git add .
$ git commit -m "Added username and change sign_in to use username or email"

The source code for this tutorial can be found on Github at:

http://github.com/wordplay/template_devise_site

Leave a Reply

preload preload preload