Authentication with Devise and cancancan in Rails 4.2

This is a beginner level tutorial on how to set up authentication (verifying who you are) and authorization (what you are permitted to do) using Ruby 2.2, Rails 4.2 and two popular Ruby gems: Devise and cancancan.

The code for this tutorial is on GitHub: https://github.com/hibbard-eu/authentication-with-devise-and-cancancan

The Scenario

The app we’ll be coding is a store (lame, I know). In order for people to use the store, they’ll need to register an account. The store will also have sellers (otherwise it would be a rubbish store) and an admin.

This means we’ll need the following resources: Item, User, Role.

Here’s a UML diagram showing how they relate to one another:

UML diagram depicting the assosciiations between the three resources in the app

Note that users can have a maximum of one role. The permissions for each of these users will break down as follows:

  • Unregistered users are redirected to the sign up page
  • Registered users: can view items
  • Sellers: can view items, create items, as well as update and destroy any items that belong to them
  • Admin: can perform any CRUD operation on any resource

So let’s get started.

If you’re wondering what price:decimal{5,2} does, it generates the following in the migration file:

.decimal :price, precision: 5, scale: 2

A decimal with a precision of 5 and a scale of 2 can range from -999.99 to 999.99.

This will create a bunch of boilerplate code. We’ll need to edit the view templates as shown below:

Remove <p id="notice"><%= notice %></p> from the top of the “index” and “show” templates in items, users and roles.

In items/index.html.erb and items/show.html.erb, change:
“User” to “Seller” and item.user to item.user.name

In items/_form.html.erb remove the complete user_id field (including the surrounding div tags)

In users/index.html.erb and users/show.html.erb change user.role to user.role.name

In users/_form.html.erb change <%= f.text_field :role_id %> to:

<%= collection_select(:user, :role_id, Role.all, :id, :name, {prompt: true}) %>

At this point if you start up Webbrick (rails s) and visit http://localhost:3000/items, /roles, or /users, you can see that our basic scaffolding is working (albeit without any data) and all of the pieces are in place for implementing the authentication logic.

Authentication with Devise

Devise is a full-featured authentication solution for Rails based on Warden. One of the things I like about it most is that it is built on a modularity concept which makes it easy to include only those features you need in your applications (although in this tutorial I will go with the default configuration).

To get started with Devise, add it to our project’s Gemfile:

gem "devise"

and run bundle install.

Then run the Devise’s installation generator:

rails g devise:install

This command generates a couple of files, an initializer and a locale file that contains all of the messages that devise needs to display.

As per the post installation message, we’ll need to make a couple of alterations to our config files.

Add the following line to the bottom of config/environments/development.rb:

config.action_mailer.default_url_options = {:host => 'localhost:3000'}

And set a root route in /config/routes.rb

root to: "items#index"

We’re going to use the User model to handle authentication and devise provides a generator for doing just that:

rails g devise User
rake db:migrate

Devise won’t override our current User model – it will simply add a bunch of attributes to it.

Edit the /app/views/layouts/application.html.erb to include the following immediately before the <%= yield %>:

And add the following line to the top of ItemsController, RolesController and UsersController:

before_filter :authenticate_user!

This will ensure that the user is logged in before being able to access any of these resources.

Now is the time to add some data. Alter db/seeds.rb thus:

Then run:

rake db:seed

Now, we can start up webbrick and log in using the email address and password of one of the users we defined in the seeds file. Exciting times, huh?

A Bit More About Devise

As I mentioned briefly above, Devise is based on a modularity concept. To elucidate that, take a peek in the User model. You should see:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
  belongs_to :role
end

In its default configuration, Devise comes with six of ten modules enabled.

You can see these in action in our app – for example Rememberable remembers the user’s authentication in a saved cookie (embodied by the checkbox on the login form that says “Remember me”).

If you didn’t require this functionality, simply comment out :rememberable and it won’t be included.

I would encourage you to take the time to read through what all of the modules do. They are listed on the project’s readme

Now, to get our app working properly, we’ll going to need to make a few tweaks.

Let’s start by namespacing the CRUD interface. This is necessary as otherwise the user registration routes and user managing routes can conflict. Alter config/routes.rb like so:

devise_for :users
scope "/admin" do
  resources :users
end

Then, in the User model, we can add the missing association (app/models/user.rb):

has_many :items

And in the ItemsController, make sure that a user is assosciated with each item before it is saved:

def create
  @item.user_id = current_user.id
  ...
end

Once you have restarted the server, you will be able to manage users at localhost:3000/admin/users.

Now, if you click on the “sign up” or “edit profile” links, you’ll notice that the user’s name attribute is missing. It would be nice to have users be able to specify their names when registering, so let’s fix that.

Devise comes with many views built-in. If we would like to customize these pages, we must transfer the Devise view files into our project so we can modify them. Luckily, Devise has a generator to do just that:

rails g devise:views

This will copy across a bunch of templates to the app/views/devise directory.

Cd into this folder, then into the sub folder registrations. Locate the files new.html.erb and edit.html.erb and add the following just after <%= devise_error_messages! %>:

<div class="field">
  <%= f.label :name %><br />
  <%= f.text_field :name %>
</div>

This will add the correct fields to the appropriate forms, but were you to attempt to fill them out at this point, you would notice that Devise isn’t saving your changes.

The reason for this is that the extra parameter you are passing to the controller (name in this case) needs to be whitelisted.

You can do this as follows in the ApplicationController:

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

Also, when a user signs up, they need to be assigned a role. We can make this default to “Regular”. And while we’re at it, we can also add some validation to make sure a user enters a name.

To do this, alter /app/models/user.rb as follows:

validates_presence_of :name
before_save :assign_role

def assign_role
  self.role = Role.find_by name: "Regular" if self.role.nil?
end

Take a moment at this point to start up the app again and make sure that everything is working. It is? Good.

The final thing we’re going to do, is give the admin the power to create new users via the admin interface, as well as to edit any of the existing users’ attributes.

First off, let’s add the user’s email address to the views.

app/views/users/index.html.erb:

<th>Email</th>
<td><%= user.email %></td>

app/views/users/show.html.erb:

<p>
  <strong>Email:
  <%= @user.email %>
</p>

app/views/users/_form.html.erb:

<div class="field">
  <%= f.label :email %><br>
  <%= f.text_field :email %>
</div>

In the form partial, we can also add a password field:

<div class="field">
  <%= f.label :password %><br>
  <%= f.password_field :password, placeholder: "Leave blank if unchanged" %>
</div>

Now things get a little complicated, so as to be able to create a new user via our admin interface (http://localhost:3000/admin/users/new), we need to whitelist the attributes that devise requires:

/app/controllers/users_controller.rb

def user_params
  params.require(:user).permit(:email, :password, :password_confirmation, :name, :role_id)
end

That’s ok, but if we try and edit an existing user (e.g. http://localhost:3000/admin/users/1/edit), then we are prompted to set a new password every time we want to update something. This is not ideal.

To get around this, we can take advantage of Devise’s update_without_password method.

Alter the update method in UsersController as shown (making sure to include the protected method needs_password?):

Before we finish, to give us a little more info on the user, add the following to /app/views/users/show.html.erb:

<p>
  <strong>Joined on:</strong>
  <%= @joined_on %>
</p>
<p>
  <strong>Last logged in on:</strong>
  <%= @last_login %>
</p>
<p>
  <strong>No. times logged in:</strong>
  <%= @user.sign_in_count %>
</p>

And the logic to the UsersController:

def show
  @joined_on = @user.created_at.to_formatted_s(:short)
  if @user.current_sign_in_at
    @last_login = @user.current_sign_in_at.to_formatted_s(:short)
  else
    @last_login = "never"
  end
end

Also, to show us which users are associated with a particular role add the association to the Role model.

app/models/role.rb:

has_many :users

Add the following to app/views/roles/show.html.erb:

<p>
  <strong>Assosciated users:</strong>
  <%= @assosciated_users %>
</p>

And the logic to the RolesController:

def show
  if @role.users.length == 0
    @assosciated_users = "None"
  else
    @assosciated_users = @role.users.map(&:name).join(", ")
  end
end

And there we go. In not very much code we have implemented a robust authentication solution for our app, as well as building a simple interface to administer users.

Take a moment to restart the app and have a play with what we’ve got so far.

Authorization With cancancan

Cancancan is an authorization library for Ruby on Rails which restricts what resources a given user is allowed to access. It is the continuation of the now defunct cancan project started by Ryan Bates (of Railscast fame).

To install it, add the following line to your Gemfile:

gem 'cancancan', '~> 1.10'

and run bundle install.

CanCanCan expects a current_user method to exist in the controller, which we have thanks to Devise.

It also expects user permissions to be defined in an Ability class, for which it includes a generator:

rails g cancan:ability

This will generate a file at app/models/ability.rb. Edit it like so:

class Ability
  include CanCan::Ability

  def initialize(user)
    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end

As you can see, abilities are defined with the method can. This method takes two parameters: the first is the action that we want to perform and the second is the model class that the action applies to. In the above example we are permitting the admin to perform any CRUD action on any resource within our app and restricting all other users to just being able to read.

We don’t care about the permissions of those users who have not yet logged in (you remember they should just be directed to the sign in page), but if we did (for example, if they could view items), you would have to initialize the user variable to something sensible, or you would end up checking for nil values all over the place.

def initialize(user)
  user ||= User.new # Guest user
  ...
end

Next we need to define an admin? method in our User model.

app/models/user.rb:

def admin?
  self.role.name == "Admin"
end

And in the UsersController we need to specify which users are authorized to do what. You can do this on a per action basis using the authorize! method, which in turn performs the can? check and raises an exception if needed.

For example to check if a given user can edit an item:

def edit
  authorize! :edit, @item
end

However, repeating this across every action in our app would soon become tiresome. Luckily there is an easier way to do this if you are using RESTful style controllers, namely with the method load_and_authorize_resource. As the name suggests, this method loads the appropriate resource and authorizes it in a before filter.

Place it at the top of your ItemsController.

load_and_authorize_resource

As this method loads the necessary resource for us based on the action we are performing, we can also remove any lines of code that set the instance variable in each action.

In the case of our ItemsController that would mean removing the following lines:

class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :edit, :update, :destroy]

  def new
    @item = Item.new
  end

  def create
    @item = Item.new(item_params)
    ...
  end

  private
  def set_item
    @item = Item.find(params[:id])
  end
end

If you now restart the server and access localhost:3000/items as a regular user or as a seller, you will be in read only mode. As an admin, you will still be able to create, edit and delete records. Cool!

Now we need to repeat the above steps for our other two resources:

app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]
  load_and_authorize_resource

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    ...
  end

  private
  def set_user
    @user = User.find(params[:id])
  end
end

app/controllers/roles_controller.rb

class RolesController < ApplicationController
  before_action :set_role, only: [:show, :edit, :update, :destroy]
  load_and_authorize_resource

  def new
    @role = Role.new
  end

  def create
    @role = Role.new(role_params)
    ...
  end

  private
  def set_role
    @role = Role.find(params[:id])
  end
endv

This ensures that regular users and sellers are also in read-only mode when accessing Roles and Users.

Next, we can ensure that our users see a nicer error page than the one with the AccessDenied exception they currently see when attempting to access something they are not authorized to.

We can do this using a method called rescue_from that we can place in our ApplicationController. We’ll pass it a block inside of which we’ll make the application show a flash error message and redirect to the home page.

app/controllers/application_controller.rb

rescue_from CanCan::AccessDenied do |exception|
  flash[:error] = "Access denied!"
  redirect_to root_url
end

Now all that remains to do is to set the permissions of regular users and sellers to something sensible.

Let’s define two more methods to check the user’s current role:

app/models/user.rb

def seller?
  self.role.name == "Seller"
end
def regular?
  self.role.name == "Regular"
end

Then it’s a matter of updating the abilities in the Ability class.

app/models/ability.rb

if user.admin?
  can :manage, :all
elsif user.seller?
  can :read, Item
  can :create, Item
  can :update, Item do |item|
    item.try(:user) == user
  end
  can :destroy, Item do |item|
    item.try(:user) == user
  end
elsif user.regular?
  can :read, Item
end

The permissions for admins and regular users should be straight forward.

In the case of sellers however, things become a little more complicated when dealing with the update and destroy actions (you remember that sellers should only be able to update and delete their own items).

Here we pass can a block which will pass in the instance of the model we’re checking. The block should return true or false depending on whether the action should be allowed, or not. Within the block we’ll use Rails’ try method to check that the item’s user attribute is the current user. Using try covers the eventuality that the item is nil and will prevent an exception being raised.

Finally, so that regular users and sellers don’t see any links to actions they are not permitted to perform, alter app/views/items/index.html.erb like so:

<td>
  <% if can? :update, item %>
    <%= link_to 'Edit', edit_item_path(item) %>
  <% end %>
</td>

<td>
  <% if can? :destroy, item %>
    <%= link_to 'Destroy', item, method: :delete, data: { confirm: 'Are you sure?' } %>
  <% end %>
</td>

<% if can? :create, Item %>
  <%= link_to 'New Item', new_item_path %>
<% end %>

The Finishing Touches

The very last thing I want to examine (promise!) is a slightly lesser documented feature of Devise. You remember that we initially wanted users who are not logged in to be redirected to the sign-in page? Well, wouldn’t it be nicer if we directed them to a welcome page with a link to either register or sign in?

To do this we need a WelcomeController and a corresponding view:

app/controllers/welcome_controller.rb

class WelcomeController < ApplicationController
  def index
  end
end

app/views/welcome/index.html.erb

<h1>Welcome to the Store!</h1>
<h2>Selling you useless crap since 2015</h2>

Once that is in place, alter your config/routes.rb to take advantage of Devise’s authenticated route thus:

authenticated :user do
  root :to => 'items#index', as: :authenticated_root
end
root :to => 'welcome#index'

Restart your server and we’re done!

In case you missed it at the beginning, the code for this tutorial is on GitHub: https://github.com/hibbard-eu/authentication-with-devise-and-cancancan

Reference:

So, this was quite a long post — I hope it proves useful for people. If you have any questions or observations, I’d be glad to hear them in the comments.

If you made it this far then you might consider following me on Twitter:

This post currently has 37 responses

  1. kohi says:

    I was able to confirm that it works this tutorial.
    Thank you from Japan
    🙂

  2. Shawna says:

    Great tutorial!

    One place where you could make this even simpler is by using enums (http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html) for the roles. That way, admin? just works out of the box. Depending on how dogmatically you want to keep concerns separated, you could eliminate the whole Roles controller too, if you wanted.

  3. Shawna says:

    I’ve found one error:

    You say about can :manage, :all: “In the above example we are permitting the admin to perform any CRUD action on any resource within our app and restricting all other users to just being able to read.”

    From CanCanCan’s “Defining Abilities” wiki page:

    “Important notice about :manage. As you read above it represents ANY action on the object.”

    Thus, it’s not true to say that can :manage :all represents only CRUD actions. If you want to allow access to only CRUD actions, you need a custom alias like:

    alias_action :create, :read, :update, :destroy, to: :crud

    • admin says:

      Hi Shawna,

      I will have a look at enums – I hadn’t thought of using them.

      Regarding your second point, I think I just worded it badly. I didn’t mean to imply that the admin is restricted to performing CRUD actions, rather, of the available CRUD actions, he or she can perform all of them.

      I don’t mind changing the text if you can think of a way to say that better.

      Thanks for taking the time to comment. I appreciate it!

  4. Cristiano Betta says:

    Nice article. I found a small issue: you scaffold the User before the Role, which on my machine throws an error when migrating (my default DB is PG).

    • admin says:

      Thank you for pointing that out.
      Did you find a solution to the problem?

      I’m unable to reproduce the error (using SQLite), but I would be happy to update the article if you have a workaround.

  5. Jorge says:

    I get the following error when I go through the tutorial and also when I clone the github repository.

    undefined method `name' for nil:NilClass
    Extracted source (around line #16):
    
    def admin?
      self.role.name == "Admin"
    end
    def seller?
    ...
    

    The line is: self.role.name == "Admin"

    Has anybody encountered this error?

    • admin says:

      Sorry to hear that you are having problems.

      I just checked everything is working. I ran:

      git clone https://github.com/hibbard-eu/authentication-with-devise-and-cancancan.git
      cd authentication-with-devise-and-cancancan/
      bundle
      rake db:migrate
      rake db:seed
      rails s

      And everything worked as expected.
      I wouldn’t have thought that there was anything Rails 4 or Ruby 2.2 specific in there, but might it be worth checking your versions?

      Otherwise, if there’s anything else I can do to help, just let me know.

    • satish says:

      I found same error.

  6. Jorge says:

    Thank you, admin!

    I followed your steps to clone the project and it works as expected.

    The problem I had was caused by not running the rake db:seed command. If I ommit that, whenever I try to sign up the app throws the error.

    Thank you so much for your help!

    Jorge

  7. Jorge says:

    Thank you!

    I am having another problem. When I try to add a new item as admin I get the following error when I hit the update button:

    undefined method `name' for nil:NilClass

    Are you able to save items?

    Thank you!
    Jorge

    • admin says:

      Oops, no, this was a bug. The User association isn’t being saved when the Item is saved.

      You can solve this by adding the following to the top of the create action in the ItemsController:

      @item.user_id = current_user.id

      I’ll update the article to refelct this.
      Thanks!

  8. Alfredo says:

    Hello, I found your tutorial very useful. I’ve just a problem: when I type localhost:3000 I get an error page with the following message: “Template is missing
    Missing template welcome/index”, even if I’ve added the page /app/views/welcome/index.html.rb, and the routes.rb file like this:

    authenticated :user do
      root :to => 'items#index', as: :authenticated_root
    end
    root :to => 'welcome#index'
    • Alfredo says:

      Solved, was a stupid error! I called the Welcome view welcome.html.rb intead of welcome.html.erb. Thanks for this tutorial!

  9. ahav says:

    Thanks for a great tutorial. Instead of a store I want to build a multiuser blog. Do I just change items to posts. How do I use this tutorial to achieve my aim. Thanks

    • admin says:

      Hi,

      I’m afraid it’ll be slightly more complicated than that.
      Maybe try reading one of the numerous Rails blog tutorials available online, then use Devise & cancancan to implement the authentication/authorization.

      Good luck!

  10. satish says:

    I am using mysql as database and I am getting an error which is,

    undefined method `name' for nil:NilClass

    highlighted for –>

    def admin?
      self.role.name == "Admin"
    end

    Also FYI i am following the steps from starting …

  11. satish says:

    I made the following changes:

    guest = User.new
    guest.role = Role.new
    guest.role.name = "Regular"
    user ||= guest # Guest user

    in place of:

    user = User.new

    in the Ability class and its working now

    Also, to migrate it from sqlite to any other database just change migration order as->
    role, user, item by changing time stamp in migration files name.

  12. satish says:

    you forgot to tell some instructions such as->

    1). always use authentication and authorization in order as

    before_filter :authenticate_user!
    load_and_authorize_resource

    otherwise undefined method `name' for nil:NilClass error will occur
    2). use scaffolding in order as

    rails g scaffold role name:string description:string
    rails g scaffold user name:string role:belongs_to
    rails g scaffold item name:string description:text 'price:decimal{5,2}', user:belongs_to

    otherwise database migration fail

  13. Reyza says:

    thank’s a lot for article, I glad and successfully . .yeah

  14. Kezot says:

    heyy satish you cool dude

  15. pochocosta says:

    Hi! Very nice article! I think I found a little bug. I logged in with the user Kev, then I cancel the account, and then log in with sally and the items index crash with the error: undefined method `name’ for nil:NilClass

    I think that the problem is related with the products of the cancelled user.

    • admin says:

      Hi,

      You are quite right. When a user is deleted, the items they have been created should be removed from the database, too.

      You can do this by changing the following line in app/models/user.rb:

      has_many :items

      to:

      has_many :items, :dependent => :destroy

      I updated the repo.

  16. pochocosta says:

    Hi again! So many thanks for your reply.
    I tried to execute the tests but so many of them cause errors. I was able to solve many of them, but not all of them.
    Someone has been able to solve the “ActionView::Template::Error: ActionView::Template::Error: undefined method `name’ for nil:NilClass” in the “should get index” test of the items_controller_test.rb ?

    Thanks so much!

    • admin says:

      Hi there,

      Not sure how much sense the tests actually make (they were just the standard ones produced by the scaffold generator).

      That said, here’s how you would make all tests pass:

      In test_helper.rb add the following to the bottom of the file:

      class ActionController::TestCase
        include Devise::TestHelpers
      end

      In the controller tests we now have access to Devise’s sign_in method.

      In RolesControllerTest, ItemsControllerTest and UsersControllerTest add the following line to the top of the setup block:

      sign_in users(:one)

      Now we need to update the fixtures:

      roles.yml:

      one:
        id: 1
        name: Admin
        description: MyString
      
      two:
        id: 2
        name: MyString
        description: MyString

      users.yml:

      one:
        id: 1
        name: Bob
        role_id: 1
        email: a@test.com
      
      two:
        id: 2
        name: Bill
        role_id: 2
        email: b@test.com

      items.yml:

      one:
        name: MyString
        description: MyText
        price: 9.99
        user_id: 1
      
      two:
        name: MyString
        description: MyText
        price: 9.99
        user_id: 2

      If you run rake test at this point, they will all pass bar should create user in UsersControllerTest

      Here’s how we make that work:

      test "should create user" do
        assert_difference('User.count') do
          post :create, user: {
            name: @user.name,
            role_id: @user.role_id,
            email: "test@test.com",
            password: "11111111",
            password_confirmation: "11111111"
          }
        end
        assert_redirected_to user_path(assigns(:user))
      end

      HTH. I updated the repo with these changes.

  17. Carlos Fagiani Junior says:

    this:

    app/controllers/application_controller.html

    is this:

    app/controllers/application_controller.rb

    you only write the extension html but it is rb 🙂

  18. Carlos Fagiani Junior says:

    Hi Admin,
    before_filter is deprecated in version>2.6.8, it has been replaced by before_action. It is only a information. But your code works fine. : )

    I trying to show “item#index” for no logged users, I try to use:

    before_action :authenticate_user!, except: [:index, :show]

    But it crash on ability verification.

    I add in ability a if:

    if user.id.nil?
          can :read, Item
    ...
    

    But it no sound good for me. Is it correct? or is it a bad smell?

    • admin says:

      I would have thought

      before_action :authenticate_user!, except: [:index, :show]

      would have worked. How is it crashing?

  19. Gate Keeper says:

    This works… Fantastic job !!!

Pingbacks:

Comments are closed!

I'm on Twitter:

Categories