The Rails 4 Way (2014)
Chapter 14. Authentication and Authorization
“Thanks goodness [sic], there’s only about a billion of these because DHH doesn’t think auth/auth [sic] belongs in the core.”
—George Hotelling at http://del.icio.us/revgeorge/authentication
If you’re building a web application, more often than not you will likely need some form of user security. User security can be broken up into two categories, authentication which verifies the identity of a user, and authorization which verifies what they can do.
In version 3.1, Rails introduced has_secure_password, which adds methods to set and authenticate against a BCrypt password. Although this functionality now exists in the framework, it is only a small part of a robust authentication solution. We still need to write our own authentication code or have to look outside of Rails core for a suitable solution.
In this chapter, we’ll cover authentication library Devise, writing your own authentication code with has_secure_password, and cover the authorization library pundit.
14.1 Devise
Devise74 is a highly modular Rack-based authentication framework that sits on top of Warden. It has a robust feature set and leverages the use of Rails generators, and you only need to use what is suitable for your application.
14.1.1 Getting Started
Add the devise gem to your project’s Gemfile and bundle install. Then you can generate the Devise configuration by running:
1 $ rails generate devise:install
This will create the initializer for devise, and an English version i18n YAML for Devise’s messages. Devise will also alert you at this step to remember to do some mandatory Rails configuration if you have not done so already. This includes setting your default host for Action Mailer, setting up your root route, and making sure your flash messages will render in the application’s default layout.
14.1.2 Modules
Adding authentication functionality to your models using Devise is based on the concept of adding different modules to your class, based on only what you need. The available modules for you to use are:
database-authenticatable
Handles authentication of a user, as well as password encryption.
confirmable
Adds the ability to require email confirmation of user accounts.
lockable
Can lock an account after n number of failed login attempts.
recoverable
Provides password reset functionality.
registerable
Alters user sign up to be handled in a registration process, along with account management.
rememberable
Provides remember me functionality.
timeoutable
Allows sessions to be expired in a configurable time frame.
trackable:
Stores login counts, timestamps, and IP addresses.
validatable
Adds customizable validations to email and password.
omniauthable
Adds Omniauth support
Knowing which modules you wish to include in your model is important for setting up your models, migrations, and configuration options later on.
14.1.3 Models
To set up authentication in a model, run the Devise generator for that model and then edit it. For the purpose of our examples, we will use the ever-so-exciting User model.
$ rails generate devise User
This will create your model, a database migration, and route for your shiny new model. Devise will have given some default modules to use, which you will need to alter in your migration and model if you want to use different modules. In our example we only use a subset of what is offered.
Our resulting database migration looks like
1 classDeviseCreateUsers < ActiveRecord::Migration
2 def change
3 create_table(:users) do |t|
4 ## Database authenticatable
5 t.string :email, null: false, default: ""
6 t.string :encrypted_password, null: false, default: ""
7
8 ## Recoverable
9 t.string :reset_password_token
10 t.datetime :reset_password_sent_at
11
12 ## Rememberable
13 t.datetime :remember_created_at
14
15 ## Trackable
16 t.integer :sign_in_count, default: 0
17 t.datetime :current_sign_in_at
18 t.datetime :last_sign_in_at
19 t.string :current_sign_in_ip
20 t.string :last_sign_in_ip
21
22 ## Confirmable
23 # t.string :confirmation_token
24 # t.datetime :confirmed_at
25 # t.datetime :confirmation_sent_at
26 # t.string :unconfirmed_email # Only if using reconfirmable
27
28 ## Lockable
29 # t.integer :failed_attempts, default: 0 # Only if lock strategy
30 # is :failed_attempts
31 # t.string :unlock_token # Only if unlock strategy is :email or :both
32 # t.datetime :locked_at
33
34 t.timestamps
35 end
36
37 add_index :users, :email, unique: true
38 add_index :users, :reset_password_token, unique: true
39 # add_index :users, :confirmation_token, unique: true
40 # add_index :users, :unlock_token, unique: true
41 end
42 end
We then modify our User model to mirror the modules we included in our migration.
1 classUser < ActiveRecord::Base
2 # Include default devise modules. Others available are:
3 # :confirmable, :lockable, :timeoutable and :omniauthable
4 devise :database_authenticatable, :registerable,
5 :recoverable, :rememberable, :trackable, :validatable
6 end
Now we’re ready to rake db:migrate and let the magic happen.
14.1.4 Controllers
Devise provides some handy helper methods that can be used in your controllers to authenticate your model or get access to the currently signed in person. For example, if you want to restrict access in a controller you may use one of the helpers as a before_action.
1 classMeatProcessorController < ApplicationController
2 before_action :authenticate_user!
3 end
You can also access the currently signed in user via the current_user helper method, or the current session via the user_session method. Use user_signed_in? if you want to check if the user had logged in without using the before_action.
Thais says… The helper methods are generated dynamically, so in the case where your authenticated models are named differently use the model name instead of user in the examples. An instance of this could be with an Admin model - your helpers would be current_admin, admin_signed_in?, and admin_session. |
14.1.5 Views
Devise is built as a Rails Engine, and comes with views for all of your included modules. All you need to do is write some CSS and you’re off to the races. However there may be some situations where you want to customize them, and Devise provides a nifty script to copy all of the internal views into your application.
rails generate devise_views
If you are authenticating more than one model and don’t want to use the same views for both, just set the following option in your config/initializers/devise.rb:
config.scoped_views = true
ERB to Haml The views extracted from the Devise Rails Engine are ERB templates. If your preference is to use Haml for templates, one can convert the Devise ERB templates via the html2haml gem. After the gem is installed, run the following command from the root of your Rails project: $ for file in app/views/devise/**/*.erb; do html2haml -e $file ${file%erb}haml && rm $file; done |
14.1.6 Configuration
When you first set up Devise using rails generate devise:install, a devise.rb was tossed into your config/initializers directory. This initializer is where all the configuration for Devise is set, and it is already packed full of commented-out goodies for all configuration options with excellent descriptions for each option.
Durran says… Using MongoDB as your main database? Under the general configuration section in the initializer switch the require of active---record to mongoid for pure awesomeness |
Devise comes with internationalization support out of the box and ships with English message definitions located in config/locales/devise.en.yml. (You’ll see this was created after you ran the install generator at setup.) This file can be used as the template for Devise’s messages in any other language by staying with the same naming convention for each file. Create a Chilean Spanish translation in config/locales/devise.cl.yml weon!
14.1.7 Strong Parameters
With the addition of Strong Parameters to Rails 4, Devise has followed suit and moved the concern of mass-assignment to the controller. In Devise, mass-assignment parameter sanitation occurs in the following three actions:
sign_in
Corresponding to controller action Devise::SessionsController#new, only authentication keys, such as email are permitted.
sign_up
Corresponding to controller action Devise::RegistrationsController#create, permits authentication keys, password, and password_confirmation.
account_update
Corresponding to controller action Devise::RegistrationsController#update, permits authentication keys, password, password_confirmation, and current_password.
If you require additional parameters to be permitted by Devise, the simplest way to do so is through a before_action callback in ApplicationController.
1 classApplicationController < ActionController::Base
2 before_action :devise_permitted_parameters, if: :devise_controller?
3
4 protected
5
6 def devise_permitted_parameters
7 devise_parameter_sanitizer.for(:sign_up) << :phone_number
8 end
9 end
Additionally, passing a block to devise_parameter_sanitizer, one can completely change the Devise defaults.
1 classApplicationController < ActionController::Base
2 before_action :devise_permitted_parameters, if: :devise_controller?
3
4 protected
5
6 def devise_permitted_parameters
7 devise_parameter_sanitizer.
8 for(:sign_in) { |user| user.permit(:email, :password, :remember_me,
9 :username) }
10 end
11 end
For more details on Strong Parameters, see Chapter 15, “Security”.
14.1.8 Extensions
There are plenty of 3rd party extensions out there for Devise that come in handy if you are authenticating using different methods.
cas_authenticatable
Allows for single sign on using CAS.
ldap_authenticatable
Authenticate users using LDAP.
rpx_connectable
Adds support for using RPX authentication.
A complete list of extensions can be found at: https://github.com/plataformatec/devise/wiki/Extensions
14.1.9 Testing with Devise
To enable Devise test helpers in controller specs, create the spec support file devise.rb in the spec/support folder.
1 # spec/support/devise.rb
2 RSpec.configure do |config|
3 config.include Devise::TestHelpers, type: :controller
4 end
This will add helper methods sign_in and sign_out, that allow creating and destroying a session for a controller spec respectively. Both methods accept an instance of a Devise model.
1 require 'spec_helper'
2
3 describe AuthenticatedController do
4 let(:user) { FactoryGirl.create(:user) }
5
6 before do
7 sign_in user
8 end
9
10 ...
11 end
14.1.10 Summary
Devise is an excellent solution if you want a large number of standard features out of the box while writing almost no code at all. It has a clean and easy to understand API and can be used with little to no ramp up time on any application.
14.2 has_secure_password
Prior to version 3.1, Rails did not include any sort of standard authentication mechanism. That changed with the introduction of has_secure_password, an ActiveModel mechanism that adds methods to set and authenticate against a BCrypt password75. However, has_secure_password is only a small piece to a complete authentication solution. Unlike other solutions like Devise, one still needs to implement a few extra items in order to get has_secure_password running properly.
14.2.1 Getting Started
To use Active Model’s has_secure_password, add the required gem dependency bcrypt-ruby to your Gemfile and run bundle install.
gem 'bcrypt-ruby', '~> 3.0.0'
14.2.2 Creating the Models
To add authentication to a model, it must have an attribute named password_digest. For the purpose of our example, let’s generate a new User model that will authenticate with an email and password.
$ rails generate model User email:string password_digest:string
Then edit the CreateUsers migration to add the columns your application needs to satisfy its authentication requirements.
1 classCreateUsers < ActiveRecord::Migration
2 def change
3 create_table :users do |t|
4 t.string :email
5 t.string :password_digest
6 t.timestamps
7
8 t.index(:email, unique: true)
9 end
10 end
11 end
Next, setup your User model, by adding the macro style method has_secure_password. We’ve added a uniqueness validation for email to ensure we can only have one email per user.
1 classUser < ActiveRecord::Base
2 has_secure_password
3
4 validates :email, presence: true, uniqueness: { case_sensitive: false }
5 end
A virtual attribute password is added to the model, which when set, automatically copies its encrypted value to password_digest. Validations on create for the presence and confirmation of password are also added.
To illustrate, let’s create and authenticate a user in the console:
>> user = User.create(email: 'user@example.com')
=> #<User id: nil, email: "user@example.com", password_digest: nil,
created_at: nil, updated_at: nil>
>> user.valid?
=> false
>> user.errors.full_messages
=> ["Password can't be blank"]
>> user = User.create(email: 'user@example.com', password: 'therails4way',
password_confirmation: 'therails4way')
=> #<User id: 1, email: "user@example.com", password_digest:
"$2a$10$RZfWUZiGze9Bk13PFOYB5eWKZuJUMAnqU/90rpcywGja...",
created_at: "2013-10-01 15:26:55", updated_at: "2013-10-01 15:26:55">
>> user.authenticate('abcdefgh')
=> false
>> user.authenticate('therails4way')
=> #<User id: 1, email: "user@example.com", password_digest:
"$2a$10$RZfWUZiGze9Bk13PFOYB5eWKZuJUMAnqU/90rpcywGja...",
created_at: "2013-10-01 15:26:55", updated_at: "2013-10-01 15:26:55">
14.2.3 Setting Up the Controllers
Once the User model has been setup, we need to create a sessions controller to manage the session for your authenticated model. A resourceful controller for “users” is also required, but its implementation will depend on your own application’s requirements.
To create the controllers, run the following in the terminal:
$ rails generate controller sessions
$ rails generate controller users
In your ApplicationController you will need to provide access to the current user, so that all of your controllers can access this information easily.
1 classApplicationController < ActionController::Base
2 protect_from_forgery with: :exception
3
4 helper_method :current_user
5
6 protected
7
8 def current_user
9 @current_user ||= User.find(session[:user_id]) if session[:user_id]
10 end
11 end
The SessionsController should respond to new, create, and destroy in order to leverage all basic sign-in/out functionality.
1 classSessionsController < ApplicationController
2 def new
3 end
4
5 def create
6 user = User.where(email: params[:email]).first
7
8 if user && user.authenticate(params[:password])
9 session[:user_id] = user.id
10 redirect_to root_url, notice: 'Signed in successfully.'
11 else
12 flash.now.alert = 'Invalid email or password.'
13 render :new
14 end
15 end
16
17 def destroy
18 session[:user_id] = nil
19 redirect_to root_url, notice: 'Signed out successfully.'
20 end
21 end
Make sure you’ve added the routes for the new controllers.
1 Rails.application.routes.draw do
2 resource :session, only: [:new, :create, :destroy]
3 resources :users
4 ...
5 end
Finally, create a view app/views/sessions/new.html.haml containing a sign-in form to allow users to create a session within your application:
1 %h1 Sign in
2
3 - if flash.alert
4 .alert= flash.alert
5
6 = form_tag session_path do
7 .field
8 = label_tag :email
9 = email_field_tag :email, params[:email],
10 placeholder: 'Enter your email address', required: true
11
12 .field
13 = label_tag :password
14 = password_field_tag :password, params[:password],
15 placeholder: 'Enter your password', required: true
16
17 = submit_tag 'Sign in'
14.2.4 Controller, Limiting Access to Actions
Now that you are authenticating, you will want to control access to specific controller actions. A common pattern for handling this is through the use of action callbacks in your controllers, where the authentication checks reside in your ApplicationController
1 classApplicationController < ActionController::Base
2 ...
3
4 protected
5
6 def authenticate
7 unless current_user
8 redirect_to new_session_url,
9 alert: 'You need to sign in or sign up before continuing.'
10 end
11 end
12 end
13
14 classDashboardController < ApplicationController
15 before_action :authenticate
16 end
14.2.5 Summary
We’ve only scratched the surface of implementing a full blown authentication solution using has_secure_password. Although the implementation is simple, it leaves a bit to be desired. Some things to consider when creating your own authentication framework from scratch include “remember me” functionality, the ability for a user to reset a password, token authentication, and so on.
14.3 Pundit
Authorization is the function of specifying access rights to resources76, such as models. Once a user has been authenticated within an application, using authorization, one can limit a user from performing certain actions, for instance updating a record. Besides actions, one could even limit what is visible to a user based on their role. For example, if we created a blog application, a normal user should only be able to view published posts, while an administrator should be able to view all posts within the application.
Pundit77 is a minimal authorization library created by the folks at Elabs, that is focused around a notion of policy classes. A policy is a class that has the same name as a model class, suffixed with the word “Policy”. It accepts both a user and model instance, that are used to determine if the provided user has permissions to perform certain actions.
Kevin Says… The second argument to a Pundit policy can by any object, not necessarily just an Active Record instance. |
14.3.1 Getting started
Add the pundit gem to your project’s Gemfile and bundle install. Then you can install Pundit by running the pundit:install generator:
$ rails generate pundit:install
This will create an application policy in app/policies for Pundit. Although optional, inheriting from ApplicationPolicy for each of your policy files is recommended, as it ensures by default no resourceful action is authorized.
1 # app/policies/application_policy.rb
2 classApplicationPolicy
3 attr_reader :user, :record
4
5 def initialize(user, record)
6 @user = user
7 @record = record
8 end
9
10 def index?
11 false
12 end
13
14 def show?
15 scope.where(id: record.id).exists?
16 end
17
18 def create?
19 false
20 end
21
22 def new?
23 create?
24 end
25
26 def update?
27 false
28 end
29
30 def edit?
31 update?
32 end
33
34 def destroy?
35 false
36 end
37
38 def scope
39 Pundit.policy_scope!(user, record.class)
40 end
41 end
Next, to include the Pundit methods within a controller, include Pundit in your ApplicationController:
1 classApplicationController < ActionController::Base
2 include Pundit
3 end
14.3.2 Creating a Policy
To create a policy for a model, run the Pundit generator for that model and then edit it. To illustrate, we will use the Post model from the preceding example of a blog application.
$ rails generate pundit:policy post
The generator creates the following PostPolicy in the app/policies folder:
1 classPostPolicy < ApplicationPolicy
2 classScope < Struct.new(:user, :scope)
3 def resolve
4 scope
5 end
6 end
7 end
In the case of our example, let’s guard against non-administrator users from creating a blog post by implementing the create? predicate method.
1 classPostPolicy < ApplicationPolicy
2 def create?
3 user.admin?
4 end
5 ...
6 end
Besides checking against a role, one can add permission conditions based on the record itself. For example, in this blogging application, an administrator can only delete a post if it hasn’t been published.
1 classPostPolicy < ApplicationPolicy
2 def destroy?
3 user.admin? && !record.published?
4 end
5 ...
6 end
14.3.3 Controller Integration
Pundit provides various helper methods to be used in controllers to authorize a user to perform an action against a record. For example, the authorize method will automatically infer the policy file based on the passed in record instance. To illustrate, let’s check if the current user can create a post within the PostsController:
1 classPostsController < ApplicationController
2 expose(:post)
3
4 def create
5 authorize post
6 post.save
7 respond_with(post)
8 end
9
10 ...
11 end
The above call to authorize is equivalent to PostPolicy.new(current_user, @post).create?. If the user is not authorized, Pundit will raise a NotAuthorizedError exception.
Note
The authorize method will gain access to the currently logged in user by calling the current_user method. This can be overridden by implementing a method called pundit_user in your controller.
If you want to ensure authorization is always executed within your controllers, Pundit also provides a method verify_authorized that raises an exception if authorize hasn’t been called. This method should be run within an after_action callback.
1 classApplicationController < ActionController::Base
2 after_filter :verify_authorized, except: :index
3 end
14.3.4 Policy Scopes
Using Pundit, we can define a scope within a policy to limit what records are returned based on a user role. For example, in our recurring blogging application example, an administrator should be able to view all posts, whereas a user should only be able to view posts that have been published. This is achieved by implementing a nested class named Scope under the policy class. The instances of the scope must respond to the method resolve, which should return an ActiveRecord::Relation.
1 classPostPolicy < ApplicationPolicy
2 classScope < Struct.new(:user, :scope)
3 def resolve
4 if user.admin?
5 scope
6 else
7 scope.where(published: true)
8 end
9 end
10 end
11 ...
12 end
Pundit provides a helper method policy_scope that infers the policy file based on the class passed into it, and return the scope specific to the current user’s permissions.
1 def index
2 @posts = policy_scope(Post)
3 end
which is equivalent to
1 def index
2 @posts = PostPolicy::Scope.new(current_user, Post).resolve
3 end
To ensure policy scopes are always called for specific controller actions, run verify_policy_scoped in an after_action callback. If policy_scope is not called, an exception will be raised.
1 classApplicationController < ActionController::Base
2 after_filter :verify_policy_scoped, only: :index
3 end
14.3.5 Strong Parameters
Pundit also makes it possible to explicitly set what attributes are allowed to be mass-assigned with strong parameters based on a user role.
1 # app/policies/assignment_policy
2 classAssignmentPolicy < ApplicationPolicy
3 def permitted_attributes
4 if user.admin?
5 [:title, :question, :answer, :status]
6 else
7 [:answer]
8 end
9 end
10 end
11
12 # app/controllers/assignments_controller.rb
13 classAssignmentsController < ApplicationController
14 expose(:assignment, attributes: :assignment_params)
15
16 def update
17 assignment.save
18 respond_with(assignment)
19 end
20
21 private
22
23 def assignment_params
24 params.require(:assignment).
25 permit(policy(assignment).permitted_attributes)
26 end
27 end
14.3.6 Testing Policies
Although Pundit comes with its own RSpec matchers for testing, our preference is to use an RSpec matcher created by the team at Thunderbolt Labs78 as it provides better readability.
To get started, add the following into a file under spec/support:
1 # spec/support/matchers/permit_matcher.rb
2 RSpec::Matchers.define :permit do |action|
3 match do |policy|
4 policy.public_send("#{action}?")
5 end
6
7 failure_message do |policy|
8 "#{policy.class} does not permit #{action} on #{policy.record} for
9 #{policy.user.inspect}."
10 end
11
12 failure_message_when_negated do |policy|
13 "#{policy.class} does not forbid #{action} on #{policy.record} for
14 #{policy.user.inspect}."
15 end
16 end
Using the above RSpec matcher, one can test policies that look like
1 # spec/policies/post_policy.rb
2 require 'spec_helper'
3
4 describe PostPolicy do
5 subject(:policy) { PostPolicy.new(user, post) }
6
7 let(:post) { FactoryGirl.build_stubbed(:post) }
8
9 context "for a visitor" do
10 let(:user) { nil }
11
12 it { is_expected.to permit(:show) }
13 it { is_expected.to_not permit(:create) }
14 it { is_expected.to_not permit(:new) }
15 it { is_expected.to_not permit(:update) }
16 it { is_expected.to_not permit(:edit) }
17 it { is_expected.to_not permit(:destroy) }
18 end
19
20 context "for an administrator" do
21 let(:user) { FactoryGirl.create(:administrator) }
22
23 it { is_expected.to permit(:show) }
24 it { is_expected.to permit(:create) }
25 it { is_expected.to permit(:new) }
26 it { is_expected.to permit(:update) }
27 it { is_expected.to permit(:edit) }
28 it { is_expected.to permit(:destroy) }
29 end
30 end
14.4 Conclusion
We’ve covered the most popular authentication and authorization frameworks for Rails at the moment, but there are plenty more out there to examine if these are not suited for your application. Also, you were able to see how easy it is to roll your own simple authentication solution usinghas_secure_password.