Authentication and Authorization - The Rails 4 Way (2014)

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.

tip

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

tip

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.

tip

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.

tip

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.