Devise Authentication in Depth
This is the second article in the “Authentication with Rails” series. We are going to discuss Devise, a popular, full-fledged authentication solution by Platformatec.
In comparison to Sorcery (which I looked at last time), Devise is a more high-level solution that takes care of many different aspects for you. It presents controllers, views, mailers, and routes. While it’s simple to issue a couple of commands an have Devise up and running, it is highly customizable. Devise has very thorough documentation and a large community that produces a boatload of useful extensions. No wonder Devise is so popular.
Devise comes with a handful of modules, allowing you to choose only the required ones. There is a module to support password recovery, e-mail confirmation, account lock out, and many others.
In this article I’ll go over:
- Integrating Devise into the demo app
- Setting up Devise and enabling specific modules
- Customizing Devise
- Restricting access to certain pages
- Setting up asynchronous e-mail delivery
- Integrating a Devise extension to estimate password strength
- Adding password strength estimation to client-side
Sounds good? Let’s get started!
The demo of the application is at (broken link)
The source code is on Github.
Ground Work
As in the previous article of the series, we are going to create an app that provides no functionality apart
from authentication (with some related features). I couldn’t think of any fancy name this time, so I called it simply “Devise Demo”:
$ rails new DeviseDemo -T
Rails 4.2.0 is used, but Devise is compatible with Rails 3 as well.
Drop in some gems:
Gemfile
[...]
gem 'devise', '3.4.1'
gem 'bootstrap-sass'
[...]
bootstrap-sass
is not relevant to the tutorial, but I like it for styling.
Next run
$ bundle install
Hook up Bootstrap’s styles:
application.scss
@import "bootstrap-sprockets";
@import "bootstrap";
@import 'bootstrap/theme';
and modify layout:
views/layouts/application.html.erb
[...]
<div class="container">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>">
<%= value %>
</div>
<% end %>
</div>
<%= yield :top_content %>
<div class="container">
<h1><%= yield :header %></h1>
<%= yield %>
</div>
[...]
Please note that you have to add flash rendering functionality in some way because Devise relies on it to display various messages. It is likely that you saw a message about it when you ran bundle
for the first time with Devise.
As you can see, I am using yield
here to place some extra content called top-content
and header
. Also, I added this helper method to provide the header:
application_helper.rb
[...]
def header(text)
content_for(:header) { text.to_s }
end
[...]
Let’s also set up our home page. Create PagesController
pages_controller.rb
class PagesController < ApplicationController
end
Then the view:
views/pages/index.html.erb
<% content_for :top_content do %>
<div class="jumbotron">
<div class="container">
<h1>Welcome!</h1>
<p>Register to get started.</p>
<p>
<%= link_to 'Register', new_user_registration_path, class: 'btn btn-primary btn-lg' %>
</p>
</div>
</div>
<% end %>
and the route to tie this together:
config/routes.rb
[...]
root to: 'pages#index'
[...]
Lastly, configure the default URL options for development:
config/environments/development.rb
[...]
config.action_mailer.default_url_options = { host: '127.0.0.1', port: 3000 }
[...]
This is required to properly generate links inside the e-mail views.
Brilliant, now it is time to integrate Devise!
Integrating Devise
First of all, run the following command to generate Devise’s configuration file and translations:
$ rails generate devise:install
config/initializers/devise.rb contains a lot of different configuration options that are nicely documented. We will tweak this file in a moment. config/locales/devise.en.yml presents Devise-specific translations in English. There are plenty of translations for other languages that can be found here.
Next, generate a model with additional columns that are required by Devise:
$ rails generate devise User
You may replace User
with any other name. This command is going to create a user.rb model file and a migration that adds all the necessary fields. If the User model already exists, it will be updated.
Open up the model file to see what it contains. The most important line is:
models/user.rb
[...]
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
[...]
This is the list of Devise modules that are active for this model.
database_authenticatable
– Users will be able to authenticate with a login and password that are stored in the database. (password is stored in a form of a digest).registerable
– Users will be able to register, update, and destroy their profiles.recoverable
– Provides mechanism to reset forgotten passwords.rememberable
– Enables “remember me” functionality that involves cookies.trackable
– Tracks sign in count, timestamps, and IP address.validatable
– Validates e-mail and password (custom validators can be used).
See? Devise takes care of many different aspects for you – just choose the modules that are needed!
For this demo let’s also use two additional modules:
confirmable
– Users will have to confirm their e-mails after registration before being allowed to sign in.lockable
– Users’ accounts will be locked out after a number of unsuccessful authentication attempts.
Modify the model like this:
user.rb
[...]
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable
[...]
You also have to edit the migration file, so open it up and uncomment the following lines:
db/migrations/xxx_devise_create_users.rb
[...]
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email
t.integer :failed_attempts, default: 0, null: false
t.string :unlock_token
t.datetime :locked_at
add_index :users, :confirmation_token, unique: true
add_index :users, :unlock_token, unique: true
[...]
These fields are required for the Confirmable and Lockable modules to operate correctly. It is also a nice idea to allow users to provide their name, so add one more line:
db/migrations/xxx_devise_create_users.rb
[...]
t.string :name
[...]
Now, run the migration:
$ rake db:migrate
The users table is now created and Devise is up and running. However, we still have some things to do.
Setting Up Devise
As previously mentioned, Devise settings are present inside the devise.rb initializer file. Open it and find the “Configuration for :lockable” section. Most of the settings here will be commented out, so uncomment them and provide the following values:
config.lock_strategy = :failed_attempts
– This means that an account will be locked out after a number of unsuccessful login attempts. Actually, this is the only available strategy, but you can set this setting tonone
and handle the locking mechanism yourself.config.unlock_strategy = :both
– An account may be unlocked either via e-mail (by visiting a link provided in the email that is sent by Devise) or just by waiting a certain amount of time. Provide:email
or:time
to enable only one of these two options. Provide:none
to handle the unlocking process yourself.config.maximum_attempts = 20
– The number of times in a row the user may type an incorrect password before the account is locked out.config.unlock_in = 1.hour
– How long an account will be unlocked. Provide this setting only if you use the:time
or:both
unlock strategy.config.last_attempt_warning = true
– Issue a warning when a user has one login attempt left. This warning will be displayed as a flash message.
Great, now look for “Configuration for :confirmable” section and uncomment these settings:
config.confirm_within = 3.days
– How much time the user has to activate an account via a link sent in an e-mail (basically this means that the activation token generated by Devise will no longer be valid). If the account was not activated, a new token can be requested. Set this tonil
if you don’t want the activation token to expire.config.reconfirmable = true
– Should the user re-confirm an e-mail when it’s changed via profile update. This process is the same as the confirmation process after registration. This new, unconfirmed e-mail, is being stored in theunconfirmed_email
field until a user visits the activation link. During this period the old e-mail is used to log in.
If you are building a real application, also don’t forget to tweak those settings:
config.mailer_sender
– Provide an e-mail here that will be put to the “From” field.config.secret_key
– Secret key to generate various tokens. Modifying this will make all previously generated tokens invalid.
There are many more settings in this file that you might want to change. For example, config.password_length
that sets minimum and maximum password lengths (by default it is 8..128
).
Customizing Devise
Generating Views
All right, now Devise is set up the way we like it. Boot the server, navigate to the main page, and click the “Register” link. You will be presented with a basic form, but there are two things to notice:
- This form does not allow you to provide a name, even though we added it in the migration.
- If you are using Bootstrap for styling, the form will not look good (OK, this is not a huge problem, but let’s still take it into consideration.)
Luckily, there is a way to customize all the views provided by Devise. Run
$ rails generate devise:views
to copy the default Devise views directly into your application folder. A new folder called devise will be created inside the views directory. Let me briefly walk you through all the folders that are inside:
- confirmations – This has a lone new.html.erb view that is being rendered when a user requests to resend the confirmation e-mail.
- mailer – All the templates for emails are stored here.
- passwords – Views with forms to request password, reset email, and actually change the password.
- registrations – The new.html.erb view is rendered when a user registers on the site. edit.html.erb contains a form to update profile.
- sessions – There is only one view, which is the login form for the site.
- shared – Only one partial is present here, which contains links that are being displayed on each Devise’ page (like “Forgot your password?”, “Re-send confirmation email”, etc.)
- unlocks – Only one view with a form to request an email with an unlock link.
If you want to customize the views for certain modules only (for example, registerable
and confirmable
), run this command:
$ rails generate devise:views -v registrations confirmations
You can even have separate views for different models (if there are multiple models in your app that are equipped with Devise):
$ rails generate devise:views users
Read more here.
Registration Form
For now, let’s just change the view that provides the registration form by adding one more field and styling it up:
views/devise/registrations/new.html.erb
<% header "Sign Up" %>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<div class="form-group">
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
<% if @validatable %>
<span class="help-block"><%= @minimum_password_length %> characters minimum</span>
<% end %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
</div>
<%= f.submit "Sign up", class: 'btn btn-primary' %>
<% end %>
<%= render "devise/shared/links" %>
header
is our helper method to display page’s header. devise_error_messages!
renders any errors that were found while trying to save the record.
Restart the server and register a new user. You will now be able to provide your name.
Main Menu and Flash Messages
After being redirected back to the main page, you’ll notice some more issues:
- There is no way to identify which user is currently logged in.
- There is no way to log out.
- The welcome flash message is not styled correctly (if you are sticking with Bootstrap).
The first two issues are easily solved by adding a main menu like this:
views/layouts/application.html.erb
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'Devise Demo', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar">
<ul class="nav navbar-nav">
<li><%= link_to 'Home', root_path %></li>
</ul>
<ul class="nav navbar-nav pull-right">
<% if user_signed_in? %>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<%= current_user.name %>
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li><%= link_to 'Profile', edit_user_registration_path %></li>
<li><%= link_to 'Log out', destroy_user_session_path, method: :delete %></li>
</ul>
</li>
<% else %>
<li><%= link_to 'Log In', new_user_session_path %></li>
<li><%= link_to 'Sign Up', new_user_registration_path %></li>
<% end %>
</ul>
</div>
</div>
</nav>
user_signed_in?
is the Devise’ helper method that tells whether a user is logged in or not. current_user
returns either a user record or nil
, if no one is logged in. Please note that, if you called your model differently while generating Devise’ migration, those helper methods will also have different names! For an Admin
model, it would be admin_signed_in?
and current_admin
. Metaprogramming is cool.
All routes (apart from the root_path
, of course) are presented by Devise as well. Notice that destroy_user_session_path
(log out) requires the DELETE HTTP method by default. If you don’t like that, modify config.sign_out_via
inside the devise.rb initializer.
For styling purposes, I am taking advantage of Bootstrap’s dropdown menu. If you are using it as well, some more actions will be needed. Dropdown relies on JavaScript code and, using Turbolinks, it won’t be executed correctly when navigating between pages. Use jquery-turbolinks to fix this:
Gemfile
[...]
gem 'jquery-turbolinks'
[...]
Don’t forget to run
$ bundle install
Now modify the application.js file:
application.js
[...]
//= require jquery
//= require jquery.turbolinks
//= require jquery_ujs
//= require bootstrap/dropdown
//= require turbolinks
[...]
To style Devise-specific flash messages, use the Sass @extend
method as an easy fix (don’t overuse it, as this directive has some drawbacks):
application.scss
[...]
.alert-alert {
@extend .alert-warning;
}
.alert-notice {
@extend .alert-info;
}
[...]
Reload the server and take a look at the dropdown menu. Why isn’t the user’s name being shown?
Strong_params and Edit Profile Page
If you are using Rails with strong_params (which are enabled in Rails 4 by default), one more step is needed:
the :name
attribute has to be whitelisted.
application_controller.rb
[...]
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :name
devise_parameter_sanitizer.for(:account_update) << :name
end
[...]
We added a before_action
to all Devise controllers whitelisting the :name
attribute for sign up and account update actions (there is also :sign_in
option available). If you have a bit more complex scenario with nested hashes or arrays, refer to this section of the docs.
Before heading to the Profile page to change your name, the corresponding view should be updated:
views/devise/registrations/edit.html.erb
<% header "Edit #{resource_name.to_s.humanize}" %>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= devise_error_messages! %>
<div class="form-group">
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<span class="label label-info">Currently waiting confirmation for: <%= resource.unconfirmed_email %></span>
<% end %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
<span class="help-block">leave blank if you don't want to change it</span>
</div>
<div class="form-group">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :current_password %>
<%= f.password_field :current_password, autocomplete: "off", class: 'form-control' %>
<span class="help-block">we need your current password to confirm your changes</span>
</div>
<%= f.submit "Update", class: 'btn btn-primary' %>
<% end %>
<h3>Cancel my account</h3>
<p>Unhappy?
<%= button_to "Cancel my account", registration_path(resource_name), class: 'btn btn-danger',
data: { confirm: "Are you sure?" }, method: :delete %>
</p>
<%= link_to "Back", :back, class: 'btn btn-default btn-sm' %>
Notice that the user may also change their email address (that should be confirmed as we’ve discussed previously), password, and even delete the profile completely.
I am not going to show how the other Devise’ views are styled – browse my GitHub repo if you are interested.
Sending E-Mail and DelayedJob
Let’s briefly discuss the email sending process. First of all, you should remember that, in development environments, emails won’t be sent by default. You will still be able to see their contents in the console though. To enable sending:
config/environments/development.rb
[...]
config.action_mailer.perform_deliveries = true
[...]
and configure ActionMailer (see examples here).
Next, for production you will also have to add a similar configuration. Here is my config for the demo app:
config/environments/production.rb
[...]
config.action_mailer.delivery_method = :smtp
config.action_mailer.default_url_options = { host: 'sitepoint-devise.herokuapp.com' }
ActionMailer::Base.smtp_settings = {
:address => 'smtp.sendgrid.net',
:port => '587',
:authentication => :plain,
:user_name => ENV['SENDGRID_USERNAME'],
:password => ENV['SENDGRID_PASSWORD'],
:domain => 'heroku.com',
:enable_starttls_auto => true
}
[...]
Since this app is running on Heroku, I have to use one of its add-ons to deliver mail (you won’t be able to send e-mails directly). In my setup, Sendgrid is used. Please note that it is running on a free plan, so delivering mail may take some time.
By the way, if you want to use a custom mailer, change the config.mailer
setting inside devise.rb.
And, lastly, it may be a good idea to perform email sending in the background, otherwise users will have to wait until the mail is sent before they will be redirected to a new page. Let’s integrate Devise with DelayedJob as an example.
Drop in a new gem:
Gemfile
[...]
gem 'delayed_job_active_record'
[...]
and run
$ bundle install
$ rails generate delayed_job:active_record
$ rake db:migrate
to install the gem and generate and apply the required migration (a new table to store scheduled tasks will be created).
For Rails 4.2 modify application.rb file to set queueing backend:
config/application.rb
[...]
config.active_job.queue_adapter = :delayed_job
[...]
Now, just override Devise’ method in your model:
models/user.rb
[...]
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
[...]
deliver_later
means that the sending will be queued up. Great!
If you are going to deploy the app on Heroku, create a Procfile in the root of your project and add the following line:
Procfile
worker: rake jobs:work
Note that you will have to enable at least one worker process to handle the jobs and it will cost you about $35 per month:
$ heroku ps:scale worker=1
There is a pretty comprehensive guide about using DelayedJob on Heroku, so refer to it for more details.
Note that I won’t enable background sending on my demo app but the corresponding code is in the GitHub repo.
Restricting Access
We’ve done a good job so far, but what about restricting access to a certain pages? How can that be done?
It appears that Devise takes care of that too: you only have to use the corresponding method as a before_action
. First things first, we need a special page to restrict. Let’s call it “Secret” for simplicity:
config/routes.rb
[...]
get '/secret', to: 'pages#secret', as: :secret
[...]
A dead simple view:
views/pages/secret.html.erb
<% header "Secret!" %>
You don’t even need to add the secret
method to the PagesController
– Rails will define it implicitly.
Modify the menu a bit:
views/layouts/application.html.erb
[...]
<ul class="nav navbar-nav">
<li><%= link_to 'Home', root_path %></li>
<% if user_signed_in? %>
<li><%= link_to 'Secret', secret_path %></li>
<% end %>
</ul>
[...]
And now tweak the controller:
pages_controller.rb
[...]
before_action :authenticate_user!, only: [:secret]
[...]
This before_action
will check if the user is authenticated before calling the secret
method. Unauthenticated users will be redirected to the log in page with a “Please authenticate” flash message set. Once again note that, for models that are named differently, this method will have a different name (authenticate_admin!
for Admin
model).
Try that out!
Using Devise Extensions
The large Devise community has produced many nice extensions to add even more functionality. Let’s integrate Devise Zxcvbn by Bit Zesty as an example.
This extension is used to measure password strength and reject weak passwords. It relies on zxcvbn-ruby which, in turn, is a Ruby port of zxcvbn.js by Dropbox. Read this official blog post to find out how this solution works.
Drop in a new gem:
Gemfile
[...]
gem 'devise_zxcvbn'
[...]
and run
$ bundle install
Next register a new module for your model:
models/user.rb
[...]
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable, :zxcvbnable
[...]
Now the User
is zxcvbnable (just try to pronounce that). There is only one setting for this extension:
config/initializers/devise.rb
[...]
config.min_password_score = 0
[...]
This basically means how strong the password should be (higher is stronger). Strength is interpreted as an estimated cracking time:
0
– estimated cracking time is less than10 ** 2
seconds.1
–10 ** 4
seconds.2
–10 ** 6
seconds.3
–10 ** 9
seconds.4
– infinity (well, not really, but we can consider a couple of centuries as infinity, in this case).
I’ve set 0
here so that the users won’t have to think of complex passwords, but it is not recommended to allow such weak passwords for real apps.
Lastly, customize the error message:
config/locales/devise.en.yml
[...]
en:
errors:
messages:
weak_password: "is not strong enough. Consider adding a number, symbols or more letters to make it stronger."
[...]
Try various passwords and check how strong they are.
Measuring Passwords’ Strength on Client-Side
In conclusion, let me show you how to add zxcvbn to the client side. Download and hook up this file in your project. Create a new CoffeeScript file with the following content:
global.coffee
jQuery ->
displayHint = (strength, crack_time) ->
msg = 'Password is ' + strength + ' (time to break it: ' + crack_time + ')'
estimate_message = this.next('.estimate-message')
if estimate_message.length > 0
estimate_message.text msg
else
this.after '<span class="help-block estimate-message">' + msg + '</span>'
$('form').on 'keyup', '.estimate-password', ->
$this = $(this)
estimation = zxcvbn($this.val())
crack_time = estimation.crack_time_display
switch estimation.score
when 0 then displayHint.call($this, "very weak", crack_time)
when 1 then displayHint.call($this, "weak", crack_time)
when 2 then displayHint.call($this, "okay", crack_time)
when 3 then displayHint.call($this, "strong", crack_time)
when 4 then displayHint.call($this, "very strong", crack_time)
return
and hook it up:
application.js
[...]
//= require global
[...]
score
returns numbers from 0 to 4 – we’ve talked about them a moment ago – so it seems like the most convenient way to measure strength. crack_time_display
returns the estimated time required to crack the password in a friendly format (“3 days”, “4 years”, “centuries” etc).
Now, assign the estimate-password
class to any password field you want, for example:
views/devise/registrations/new.html.erb
[...]
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, autocomplete: "off", class: 'form-control estimate-password' %>
<% if @validatable %>
<span class="help-block"><%= @minimum_password_length %> characters minimum</span>
<% end %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control estimate-password' %>
</div>
[...]
Of course, this is the simplest solution, so feel free to extend it further.
Conclusion
That brings us to the end of this article. We’ve integrated Devise into the app, had a look at its modules and settings, and added an extension. There are many more features and extensions available, so I encourage you to take a look at the official Wiki, specifically at the How-to’s – it has more than 90 small but useful tutorials.
I hope you’ve enjoyed this second part of the Authentication with Rails series! In the next article we are going to discuss authentication with OAuth 2.
Happy coding and see you soon!