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.
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: