Rails Authentication with Devise and CanCan – Customizing Devise Controllers

I’m tired of spending loads of time creating user authentication systems with permissions or swimming against the current to customize what’s available. There’s great open source stuff out there but until now, I haven’t gotten the full package with really easy customization.

The Devise and CanCan combo for user authentication and permissions in Rails is my combo of choice.

With Devise and CanCan, you can create a customized authentication and registration process in 15 minutes, and spend another 15 minutes implementing roles and permissions.

Rails Beauty
It’s pure beauty.

Note that the code here uses Rails 3. The difference in Rails 3 and Rails 2 code for this purpose should be minimal, but please refer to the documentation for differences.

Let’s start with authentication using devise.

Step 1 – Installation

gem install devise
rails generate devise:install
rails generate devise user

Step 2 – Configuration
Configuration is super easy with Devise. Just choose which of the 11 available modules you would like to include in your authentic model (most up-to-date list here):

  1. Database Authenticatable: encrypts and stores a password in the database to validate the authenticity of an user while signing in. The authentication can be done both through POST requests or HTTP Basic Authentication.
  2. Token Authenticatable: signs in an user based on an authentication token (also known as “single access token”). The token can be given both through query string or HTTP Basic Authentication.
  3. Oauthable: adds OAuth2 support
  4. Confirmable: sends emails with confirmation instructions and verifies whether an account is already confirmed during sign in.
  5. Recoverable: resets the user password and sends reset instructions.
  6. Registerable: handles signing up users through a registration process, also allowing them to edit and destroy their account.
  7. Rememberable: manages generating and clearing a token for remembering the user from a saved cookie.
  8. Trackable: tracks sign in count, timestamps and IP address.
  9. Timeoutable: expires sessions that have no activity in a specified period of time.
  10. Validatable: provides validations of email and password. It’s optional and can be customized, so you’re able to define your own validations.
  11. Lockable: locks an account after a specified number of failed sign-in attempts. Can unlock via email or after a specified time period.

easy_button
I chose 5 of the 11 modules and configured with the following code:

# In your model
 class User < ActiveRecord::Base
    devise :database_authenticatable, :confirmable, :recoverable, :rememberable, :trackable, :validatable
end
 
# In your migration
create_table :users do |t|
    t.database_authenticatable
    t.confirmable
    t.recoverable
    t.rememberable
    t.trackable
    t.timestamps
end
 
# In your routes
devise_for :users

Step 3 – Use It!

# In your controllers
before_filter :authenticate_user!, :except => [:some_action_without_auth]
# Access Current User
def index
    @things = current_user.things
end

This simple modular approach to authentication is hot. Devise also makes it really easy for you to customize views. The out-of-the-box views are great for prototyping, but if you need more, just generate the views and edit them:

rails generate devise:views

Devise will generate all of the views it is using and place them in an app/views/devise directory. Now you have complete control over your views.

The next thing you might want to do is customize your controllers. This is a bit more tricky with devise and we’ll get to that in a minute. Right now I want to touch on permissions and then I’ll tie it all together.

Let’s consider an example where your website is in Alpha/Beta or maybe an internal tool. You want to restrict user registration to only an administrator. Enter CanCan created by Ryan Bates.

CanCan

CanCan is a great gem for implementing model permissions. The main reasons I chose CanCan are:

  • The code written to check permissions is very readable
  • The code written to declare permissions is very concise and readable
  • It keeps permission logic in a single location so it is not duplicated across controllers, views, etc.
  • Aliasing actions (read = index and show) creates more concise and readable code

Ryan Bates has a great screen cast on using CanCan here, but I do not recommend using his roles mask method (mentioned in the screen cast). It certainly works but it’s bad database design and you will feel the pain later.

After you install CanCan (instructions here), I recommend you set up a typical users HABTM roles relationship. So you end up with migrations that look like this:

class CreateRoles < ActiveRecord::Migration
  def self.up
    create_table :roles do |t|
      t.string :name
      t.timestamps
    end
  end
 
  def self.down
    drop_table :roles
  end
end
 
class UsersHaveAndBelongToManyRoles < ActiveRecord::Migration
  def self.up
    create_table :roles_users, :id => false do |t|
      t.references :role, :user
    end
  end
 
  def self.down
    drop_table :roles_users
  end
end

And your models look like this:

# User Model
class User < ActiveRecord::Base
  has_and_belongs_to_many :roles
....
# Role model
class Role < ActiveRecord::Base
  has_and_belongs_to_many :users
end

The next step is to create your Ability class that will define permissions. Mine looks like this:

class Ability
  include CanCan::Ability
 
  def initialize(user)
    user ||= User.new # guest user
 
    if user.role? :super_admin
      can :manage, :all
    elsif user.role? :product_admin
      can :manage, [Product, Asset, Issue]
    elsif user.role? :product_team
      can :read, [Product, Asset]
      # manage products, assets he owns
      can :manage, Product do |product|
        product.try(:owner) == user
      end
      can :manage, Asset do |asset|
        asset.assetable.try(:owner) == user
      end
    end
  end
end

Most of this is application specific but you can see some conveniences right away. For example, the super admin role “can manage all”. That line is saying “If the user has the super_admin role, he may perform any action on any model.” Easy enough. Also notice that the product team can “read” products and assets. This means that they can access the index or show action of either of those models. You can pass a block to the can method for more complicated permission checks, but that is beyond the scope of this post and pretty easy to figure out.

Let’s take a look at the role method. I store role names as CamelCase strings in the database but I access them with underscores which is more ruby like. The method looks like this:

def role?(role)
    return !!self.roles.find_by_name(role.to_s.camelize)
end

Tying it all together

Now let’s go back to the situation I mentioned earlier – you want to protect user registrations. This requires us to use CanCan to check for permissions but customize the Devise Registrations controller to restrict access.

One way to do this is to copy the devise controllers into your controllers directory and start customizing. That may be the best way to go and it’s certainly an obvious path, but all I want to do restrict registration. Should I really have to re-implement the registrations controller to do that? For now, I will not. It might make sense when there are more customizations. Instead I inherit from the Devise Registrations controller. Here are the steps:

Step 1 – Create the controller
$ mkdir app/controllers/users
$ touch app/controllers/users/registrations_controller.rb

Step 2 – Add the custom functionality

class Users::RegistrationsController < Devise::RegistrationsController
  before_filter :check_permissions, :only => [:new, :create, :cancel]
  skip_before_filter :require_no_authentication
 
  def check_permissions
    authorize! :create, resource
  end
end

The check permissions method is really simple. It calls the CanCan method, authorize!, and checks if the current user can create users. We use resource here because devise uses resource to refer to the model that can be authenticated. Also notice how I removed the require_no_authentication filter, a Devise filter which allows access to actions without authentication.

Step 3 – Tell your routes to go to the new controller

# replace devise_for :users with:
devise_for :users,  :controllers => { :registrations => "users/registrations" }

Step 4 – Handle the CanCan::AccessDenied exception
At this point if you hit the users/sign_up page when not logged in, you will notice that a CanCan::AccessDenied is thrown. This exception is thrown anytime permission is denied so you should customize it to your liking. I put the handler in my ApplicationController:

class ApplicationController < ActionController::Base
  ...
  rescue_from CanCan::AccessDenied do |exception|
    flash[:error] = exception.message
    redirect_to root_url
  end
  ...
end

I realize I skipped some steps in here but this post + Devise documentation + CanCan documentation should help you set up authentication with roles and permissions very quickly. Let me know if you have any questions. Enjoy!

UPDATE

A part 2 of this post is now available

No TweetBacks yet. (Be the first to Tweet this post)
Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google
  • MySpace
  • Slashdot
  • StumbleUpon
  • Technorati
  • TwitThis

If you enjoyed this post, make sure you subscribe to my RSS feed!

This entry was posted in Software and tagged , , , , , , , . Bookmark the permalink. Both comments and trackbacks are currently closed.

65 Comments

  1. Posted May 9, 2011 at 5:46 pm | Permalink

    Hello,
    I am having a bit of trouble with this…

    When I try to go to users/sign_up
    it automatically redirects to my root

    I have :registerable in my devise config in the user model

    I tried to add t.registerable to the migration, but I get an “undefined method ‘registerable’ ” error when I do rake db:migrate:redo

    Please help.

    Thank you for this tutorial.

  2. yekta
    Posted May 20, 2011 at 8:43 am | Permalink

    Thanks for the great tut Tony,

    I had better luck with the following role method for anyone interested…


    def role?(role)
    return self.roles.find_by_name(role).try(:name) == role.to_s
    end

  3. David
    Posted May 23, 2011 at 8:02 pm | Permalink

    Where do you put the Ability class? Does it go in models or does it go in lib?

  4. Posted May 23, 2011 at 11:42 pm | Permalink

    @David models

  5. Posted May 30, 2011 at 2:18 pm | Permalink

    Hey man, i’m a brazilian newbie and looking in this tuto about Devise +cancan
    - where i put in my framework the Ability Class, if i make my roles, where save this file and where i require this?
    - In what file i put this?
    def role?(role)
    return !!self.roles.find_by_name(role.to_s.camelize)
    end
    thanks

  6. Mike
    Posted June 10, 2011 at 7:54 pm | Permalink

    I am having the same issue as @Vane The Brain

    When I try to go to users/sign_up
    it automatically redirects to my root

    Is there an easy way to make registration public? Or does this go against what you are trying to do here?

  7. Posted August 15, 2011 at 1:18 am | Permalink

    Can you write a short step by step tutorial on how to use cancan.
    Please start from rails new app ……………….. rails s

    Thank you in advance

  8. Posted August 21, 2011 at 3:21 pm | Permalink

    Hey Tony,

    thx for the tut, i got some questions:

    I saw Ryan’s railscast on CanCan, and im failing to see which exact part you are referring to as “Roles mask method”, is it the idea of putting the role info into the user table as a boolean or what am i missing here?

    why exactly do you say Ryan’s method is bad DB design? isn’t it easier to fetch the role info within the user record so you don’t have to do “role-user search queries” every time you want to visualize a link (or not) or authorize something? i mean, Devise does have the current_user helper which will return the current authenticated user model, quite everywhere…

    looking forward to your answers! and thx for your time!

  9. Posted October 8, 2011 at 11:04 am | Permalink

    Small clarification for those who do not have the model name as “User”, for example in case you have the model name as “Admin”, and in console if you try Admin.find(admin_id).roles, you’ll get error saying admins_roles table does not exist, in order to avoid that your migrations should be like this instead of what you see in the tutorial:

    class UsersHaveAndBelongToManyRoles  false do |t|
          t.references :role, :admin
        end
      end
     
      def self.down
        drop_table :admins_roles
      end
    end

    or you can put the following in your models in case you don’t want to change the migration or have already created the migration:

    # Admin Model
    class Admin  'roles_admins'
    end
     
    # Role Model
    class Role  'roles_admins'
    end

    The reason for this is when Rails creates the association for the join table it expects the models to be in alphabetical order. As “r” comes after “a”, so it creates it as admins_roles.

    I had a trouble figuring it out as my Model name was “Admin” :-) , hope this helps.

    Thanks anyway great tutorial.

  10. Posted October 8, 2011 at 11:08 am | Permalink

    Grrrrrrrr, blog engine ate my code :-(

    # Admin Model
    class Admin
      has_and_belongs_to_many :roles, :join_tab le =&gt; 'roles_admins'
    end
     
    # Role Model
    class Role
      has_and_belongs_to_many :roles, :join_tab le =&gt; 'roles_admins'
    end
  11. Posted December 27, 2011 at 9:37 pm | Permalink

    Great post.

    One way you could improve it is to show how you got the CreateRoles migration. I assume you did this by running the model generator. Perhaps you could show the steps you used to get there?

  12. thomas
    Posted March 21, 2012 at 7:40 pm | Permalink

    I’m trying to restrict my users to only have access to records they create. In database, ‘datasets’, I have a field called ‘user_id’. Do I apply my query in my controller or in my Ability class? I am trying to apply the following code, but it’s not working…

    class Ability
    include CanCan::Ability

    def initialize(user)
    user ||= User.new # guest user

    if user.role? :administrator
    can :manage, :all
    elsif user.role? :fvrd
    can :manage, Dataset do |dataset|
    dataset.try(:user_id) == user.id
    end
    end
    end
    end

  13. Posted April 25, 2012 at 2:14 pm | Permalink

    Why did you put double negation infront of the find_by statement in role? method ?
    self.roles.find_by_name(role.to_s.camelize) is enough.

  14. Michael
    Posted August 19, 2012 at 11:33 am | Permalink

    I checked this and the followup post and I don’t see your Role class. I know it should be the simplest part of all this, but I’m not sure how you defined :super_admin and etc.

  15. Posted December 3, 2012 at 9:22 pm | Permalink

    Very helpful.

    I match one oDesk interview with that ;)

    But some code like migration is too old. It will be better to renew it

8 Trackbacks

  1. [...] Administrators By Tony | Published: September 29, 2010 About two months ago I wrote an article on getting started with Devise and CanCan. Since then, I’ve implemented the Devise + CanCan combo on three projects and wrote a couple [...]

  2. [...] frameworks, doesn’t have anywhere near the number of pre-written plugins to solve issues like adding roles and permissions to your site or put together an e-commerce [...]

  3. By Rails Authorization and Authentication | Matt Slay on November 27, 2010 at 8:06 pm

    [...] these great articles on another blog that describes using Rails and CanCan together: Article 1: Getting Started with Devise and CanCan The article includes very detailed steps and code samples for both Devise and CanCan.  Article [...]

  4. [...] there any database-managed roll based access control Gems on Rails 3.0?I found devise + CanCan: http://www.tonyamoyal.com/2010/0… .But its roll is hard-coded (ex. Ability class).I want to use roll based permission control gem [...]

  5. [...] from Devise’s official github page, Asciicast’s episode on CanCan by Ryan Bates, and Tony Amoyal’s tutorial. I took most of the codes from these resources and then slightly modified it to meet my own needs. [...]

  6. [...] Starting to fall for Rails A combination of Devise and Cancan might be useful, as shown herehttp://www.tonyamoyal.com/2010/0…11:35amView All 0 CommentsCannot add comment at this time. Add [...]

  7. [...] $ rails generate devise:views 10. Formate os templates das views em app/views/devise Fonte: http://www.tonyamoyal.com/2010/07/28/rails-authentication-with-devise-and-cancan-customizing-devise-... [...]

  8. [...] then went ahead and installed cancan, partly referring to this guide - the approach seemed very clean, however, I needed different sign up forms for the different user [...]