Rails and Devise
June 24, 2020 - 6 min readIn preperation for a lunch and learn for our engineering staff at Megaphone on authentication & authorization, I found myself wondering what setting up Devise looks like in Rails 6 (we've been running a Rails app that we've pushed to upgrade from 4 -> 6). Devise has great documentation to get you going, you'll find steps to setup there, but I'm captain's logging my work here as well. Without further adiue, lets dive right in.
Creating a new app
rails new sandbox
By default a rails app is fairly thin, no authentication is provided off the back.
Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.6.3'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.2', '>= 6.0.2.2'
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# Use SCSS for stylesheets
gem 'sass-rails', '>= 6'
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 4.0'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7'
# Use Active Storage variant
# gem 'image_processing', '~> 1.2'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end
group :development do
# Access an interactive console on exception pages or by calling 'console' anywhere in the code.
gem 'web-console', '>= 3.3.0'
gem 'listen', '>= 3.0.5', '< 3.2'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
group :test do
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '>= 2.15'
gem 'selenium-webdriver'
# Easy installation and use of web drivers to run system tests with browsers
gem 'webdrivers'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
Adding Devise
We'll need to add Devise since it doesn't ship with Rails.
Gemfile
gem 'devise'
Then we'll run a bundle install and the generator for devise additions.
bundle install
rails generate devise:install
With all that installed, Devise will waarn of a few things needing to be ensured
Running via Spring preloader in process 43860
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
Depending on your application's configuration some manual setup may be required:
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', port: 3000 }
In production, :host should be set to the actual host of your application.
* Required for all applications. *
2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:
root to: "home#index"
* Not required for API-only Applications *
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>
* Not required for API-only Applications *
4. You can copy Devise views (for customization) to your app by running:
rails g devise:views
* Not required *
===============================================================================
1. Ensure you have defined default url options in your environments files.
This one is asking us to place a default url for our action mailer for the development environment, adding the following suffices.
config/environments/development.rb
Rails.application.configure do
# ... existing content
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
end
2. Ensure you have defined root_url to something in your config/routes.rb.
We haven't defined any routes in our app yet.
config/routes.rb
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
Lets add a Home controller with an index.
rails generate Home index
Now we can satisfy that root_url
config/routes.rb
Rails.application.routes.draw do
get 'home/index'
root :to => "home#index"
end
3. Ensure you have flash messages in app/views/layouts/application.html.erb.
We'll just add the example snippet above the yield.
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Sandbox</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
<%= yield %>
</body>
</html>
4. You can copy Devise views (for customization) to your app
No action needed here, but good to know to break free of the default for own styling. If you want to you can run the following.
rails g devise:views
Adding Users
At this point the necessities to use Devise are aligned, but we don't have a model leveraging it. A typical approach is to use the User model for authentication purposes. We don't have that model yet, but devise can create it for us.
rails generate devise User
Now our user model has some devise modules added
app/models/user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
end
But I don't have to Log In?
At this point if you navigate to your app's root (remember we only have a Home controller), you'll see it without logging in. Devise treats authentication as an opt-in action.
If you want to add authentication to only the Home controller's routes you can use the following.
app/controllers/home_controller.rb
class HomeController < ApplicationController
before_action :authenticate_user!
def index
end
end
If you are building an app where you want all routes to require authentication, you can lift the before_action
up to the ApplicationController (root controller which others inherit from).
app/controllers/application_controller
class ApplicationController < ActionController::Base
before_action :authenticate_user!
def index
end
end
For the purposes of this example I'm going with the latter (all routes get authenticate_user!
).
Voila!
You're app now has user authentication. Devise provides a lot of functionality out of the box. We don't see
the controller/views of devise because we didnt run the view generation (rails g devise:views
) but
theres a handfull of things ready to go now. Before breaking down the modules included in the User model,
take a moment to play with the sign in, sign out, sign up, and forgotten password functionality.
From here, lets break down what devise modules were added during the generation in the User model.
app/models/user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
end
database_authenticatable
Authenticatable Module, responsible for hashing the password and validating the authenticity of a user while signing in. This module defines a
password=
method. This method will hash the argument and store it in theencrypted_password
column, bypassing any pre-existingpassword
column if it exists.
Password security is not a simple thing to self handle. Using Devise out of the box is giving enough security to shield oneself from issue like "storing passwords in the clear" because the passwords are managed in an encrypted manner. Here's an important thing to remember:
DON'T ROLL YOUR OWN CRYPTO
The security of an application is an ever-present battle. Technology advancements lead to more secure encryption, and also leads to more feasible attacks on encryption. Leveraging industry-backed encryption protocols is the smart way to go, and keeping libraries providing those protocols up to date is the safe way to go. I won't break down the aspects of password encryption in this post, rather point out what is configurable. Hopefully this aids in a google search for further clarity.
Quoting the source documentation, the DatabaseAuthenticatable module gives
us the password=
functionality on our Users model, and handles the action of
encrypting it within the database. Theres a few configurables that trickle into the
devise config file from this module, they are below.
config/initializers/devise.rb
Devise.setup do |config|
# ...
# ==> Configuration for :database_authenticatable
# For bcrypt, this is the cost for hashing the password and defaults to 12. If
# using other algorithms, it sets how many times you want the password to be hashed.
# The number of stretches used for generating the hashed password are stored
# with the hashed password. This allows you to change the stretches without
# invalidating existing passwords.
#
# Limiting the stretches to just one in testing will increase the performance of
# your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
# a value less than 10 in other environments. Note that, for bcrypt (the default
# algorithm), the cost increases exponentially with the number of stretches (e.g.
# a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
config.stretches = Rails.env.test? ? 1 : 12
# Set up a pepper to generate the hashed password.
# config.pepper = 'b0bb61ca1eb5b207fe642f0beb39cbd0b8169db03ed535ac6921af2a29b5a1b4cae3147c05d954ddd07c6b976fb605488f92fdcb36de551090ddd0dd9eea820b'
# Send a notification to the original email when the user's email is changed.
# config.send_email_changed_notification = false
# Send a notification email when the user's password is changed.
# config.send_password_change_notification = false
end
stretches
This specifies the number of times the password is hashed, for protection against a brute-force attack on a password the higher the number the better, but at a performance trade-off.
pepper
A string that is appended to the user's password to be passed to the salt generator as a step of the password hash generation.
send_email_changed_notification/send_password_change_notification
Notification flags to email the user on an update to their account.
registerable
Registerable is responsible for everything related to registering a new resource (ie user sign up).
By including this module to the User model, the "Sign Up" functionality is added to the application. The controller and view aspects of registration are only active if this module is included. A good thing to have on hand for an application which does not allow users to enroll themselves.
For a practical example, remove :registerable
from the devise call in the User model, and see
how the application reacts.
There is one configurable for this module.
config/initializers/devise.rb
Devise.setup do |config|
# ...
# ==> Configuration for :registerable
# When set to false, does not sign a user in automatically after their password is
# changed. Defaults to true, so a user is signed in automatically after changing a password.
# config.sign_in_after_change_password = true
end
recoverable
Recoverable takes care of resetting the user password and send reset instructions.
This gives the "Forgot your password?" functionality to the application. The configurables are below.
config/initializers/devise.rb
Devise.setup do |config|
# ...
# ==> Configuration for :recoverable
#
# Defines which key will be used when recovering the password for an account
# config.reset_password_keys = [:email]
# Time interval you can reset your password with a reset password key.
# Don't put a too small interval or your users won't have the time to
# change their passwords.
config.reset_password_within = 6.hours
# When set to false, does not sign a user in automatically after their password is
# reset. Defaults to true, so a user is signed in automatically after a reset.
# config.sign_in_after_reset_password = true
end
rememberable
Rememberable manages generating and clearing token for remembering the user from a saved cookie. Rememberable also has utility methods for dealing with serializing the user into the cookie and back from the cookie, trying to lookup the record based on the saved information. You probably wouldn't use rememberable methods directly, they are used mostly internally for handling the remember token.
This is the functionality that allows users to stay logged in on the given device/browser for days/weeks/months/years (please don't let them stay logged in that long). This is cookie based.
config/initializers/devise.rb
Devise.setup do |config|
# ...
# ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again.
# config.remember_for = 2.weeks
# Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true
# If true, extends the user's remember period when remembered via cookie.
# config.extend_remember_period = false
# Options to be passed to the created cookie. For instance, you can set
# secure: true in order to force SSL only cookies.
# config.rememberable_options = {}
end
validatable
Validatable creates all needed validations for a user email and password. It's optional, given you may want to create the validations by yourself. Automatically validate if the email is present, unique and its format is valid. Also tests presence of password, confirmation and length.
Adds configurable restrictions to the user's email and password. This is where you want to go if you're changing your password length/strength requirements.
Devise.setup do |config|
# ...
# ==> Configuration for :validatable
# Range for password length.
config.password_length = 6..128
# Email regex used to validate email formats. It simply asserts that
# one (and only one) @ exists in the given string. This is mainly
# to give user feedback and not to assert the e-mail validity.
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
end
What are the other Devise Modules?
Everything covered above is what was enabled on the User model by default when running the generation, but devise comes with extras! Lets look at them from a high level, disregarding the configurables for now.
Take note: when adding a devise module migrations are necessary in some instances. Take a look at the Devise wiki for needed steps, for example here is the needed migration for confirmable.
omniauthable
Adds OmniAuth support. This allows users to login using their identy with "everything from Facebook to LDAP".
confirmable
Confirmable is responsible to verify if an account is already confirmed to sign in, and to send emails with confirmation instructions. Confirmation instructions are sent to the user email after creating a record and when manually requested by a new confirmation instruction request.
This makes the user confirm their email on registration. This ensures users don't sign up with a typo/invalid email address. The user's account isn't activated until confirmed (but this is configurable).
trackable
Track information about your user sign in.
Tracks each user's sign in count, time of current sign in and last sign out, and current/last IP address.
timeoutable
Timeoutable takes care of verifying whether a user session has already expired or not. When a session expires after the configured time, the user will be asked for credentials again, it means, they will be redirected to the sign in page.
Sounds similar to rememberable? Rememberable operates on a cookie based token system to authenticate the user's actions. The clock does not reset when the user performs an action, rather they are asked to relogin on a periodic basis.
Timeoutable is a session based authentication check. This module adds tracking of
last_request_at
in the session for the user, should the user not perform
some action after N amount of time their session is terminated.
This is useful for more sensitive-data focused applications. Take a banking app for example, you wouldn't want your bank to keep you logged in for a few minutes at a time, rather you would want your bank to forcibly log you out after a few minutes of inactivity.
lockable
Handles blocking a user access after a certain number of attempts. Lockable accepts two different strategies to unlock a user after it's blocked: email and time. The former will send an email to the user when the lock happens, containing a link to unlock its account. The second will unlock the user automatically after some configured time (ie 2.hours). It's also possible to set up lockable to use both email and time strategies.
Break out those password managers, or prepare to reset your password or wait when you forget your password and inevitably try dozens of different combinations or the same password over and over and over again!
This module adds restrictions to failed password attempts.
Where to go from Here?
Everything covered up to this point is devise-provided functionality through the gem. There's an entire ecosystem of addon gems out there, go check them out and see what cool features are out there.