Devise in Rails

8 Min. Read
Aug 12, 2019

Introduction

Devise is a gem used in rails to perform a flexible authentication. The devise gem is basically based on a warden gem, which gives an opportunity to build authorization direct on a Ruby Rack Stack. This gem is pretty straight forward and well documented. Warden fetches a request data and checks if the request includes valid credentials, according to a defined strategy. If a user has access, warden establishes the request sender in an application context and then passes the request to the next part of Rails Rack Middleware Stack. If verification fails, it calls a special failure procedure, which deals with a no access case.

Modules

It’s composed of 10 modules:

  • Database Authenticatable: hashes and stores a password in the database to validate the authenticity of a user while signing in. The authentication can be done both through POST requests or HTTP Basic Authentication.
  • Omniauthable: adds OmniAuth (https://github.com/omniauth/omniauth) support.
  • Confirmable: sends emails with confirmation instructions and verifies whether an account is already confirmed during sign in.
  • Recoverable: resets the user password and sends reset instructions.
  • Registerable: handles signing up users through a registration process, also allowing them to edit and destroy their account.
  • Rememberable: manages generating and clearing a token for remembering the user from a saved cookie.
  • Trackable: tracks sign in count, timestamps and IP address.
  • Timeoutable: expires sessions that have not been active in a specified period of time.
  • Validatable: provides validations of email and password. It’s optional and can be customized, so you’re able to define your own validations.
  • Lockable: locks an account after a specified number of failed sign-in attempts. Can unlock via email or after a specified time period.

Getting started

Add devise gem

Add this line to the Gemfile

1
    gem 'devise'

and run

1
    bundle install

to install the gem. Also restart the Rails server

Set up devise in your app

Run the following command in the terminal.

1
    rails g devise:install

Configure devise

Ensure you have defined default url options in your environments files. Open up config/environments/development.rb and add this line:

1
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

before the end keyword.

The generator will install an initializer which describes ALL of Devise’s configuration options. It is imperative that you take a look at it. When you are done, you are ready to add Devise to any of your models using the generator.

Setup the User model

We’ll use a bundled generator script to create the User model.

1
    rails g devise User

This will create a model (if one does not exist) and configure it with the default Devise modules. The generator also configures your config/routes.rb file to point to the Devise controller. Next, check the MODEL for any additional configuration options you might want to add, such as confirmable or lockable. If you add an option, be sure to inspect the migration file (created by the generator if your ORM supports them) and uncomment the appropriate section. For example, if you add the confirmable option in the model, you’ll need to uncomment the Confirmable section in the migration. Then run

1
    rails db:migrate

You should restart your application after changing Devise’s configuration options (this includes stopping spring). Otherwise, you will run into strange errors, for example, users being unable to login and route helpers being undefined.

Controller filters and helpers

Devise will create some helpers to use inside your controllers and views. To set up a controller with user authentication, just add this before_action (assuming your devise model is ‘User’):

1
    before_action :authenticate_user!

Helpers

1
2
3
4
5
6
7
    user_signed_in? (to verify if the user is signed in)
    current_user (for current signed-in user this helper is used)
    user_session (access the session for this scope)
    destroy_user_session_path (Logout)
    new_user_session_path (Login)
    edit_user_registration_path (Edit registration)
    new_user_registration_path (Register new user)

Configuring Models

The Devise method in your models also accepts some options to configure its modules.

1
2
3
4
5
6
7
8
9
    class User < ActiveRecord ::Base
        devise:database_authenticatable,
            :registerable.
            :confirmable,
            :recoverable,
            :rememberable,
            :trackable,
            :validatable
    end

Configuring views

Since Devise is an engine, all its views are packaged inside the gem. These views will help you get started, but after some time you may want to change them. If this is the case, you just need to invoke the following generator, and it will copy all views to your application:

1
    rails generate devise:views

If you have more than one Devise model in your application (such as User and Admin), you will notice that Devise uses the same views for all models. Fortunately, Devise offers an easy way to customize views. All you need to do is set

1
config.scoped_views = true

inside the config/initializers/devise.rb file.

After doing so, you will be able to have views based on the role like users/sessions/new and admins/sessions/new. If no view is found within the scope, Devise will use the default view at

devise/sessions/new You can also use the generator to generate scoped views:

1
    rails generate devise:views users

If you would like to generate only a few sets of views, like the ones for the registerable and confirmable module, you can pass a list of modules to the generator with the -v flag.

1
    rails generate devise:views -v registrations confirmations

Configuring controllers

If the customization at the views level is not enough, you can customize each controller by following these steps:

  • Create your custom controllers using the generator which requires a scope:

    1
    
      rails generate devise:controllers [scope]
    

    If you specify users as the scope, controllers will be created in app/controllers/users/ And the sessions controller will look like this:

    1
    2
    3
    4
    5
    6
    7
    
    class Users::SessionsController < Devise::SessionsController
      # GET /resource/sign_in
      # def new
      #   super
      # end
      ...
    end
    

    Use the -c flag to specify a controller, for example: bash rails generate devise:controllers users -c=sessions

  • Tell the router to use this controller:

    1
    
      devise_for :users, controllers: { sessions: 'users/sessions' }
    
  • Copy the views from devise/sessions to users/sessions. Since the controller was changed, it won’t use the default views located in devise/sessions.

  • Finally, change or extend the desired controller actions.

    You can completely override a controller action:

    1
    2
    3
    4
    5
    
    class Users::SessionsController < Devise::SessionsController
      def create
        # custom sign-in code
      end
    end
    

    Or you can simply add new behaviour to it:

    1
    2
    3
    4
    5
    6
    7
    
    class Users::SessionsController < Devise::SessionsController
      def create
        super do |resource|
          BackgroundWorker.trigger(resource)
        end
      end
    end
    

    This is useful for triggering background jobs or logging events during certain actions.

Remember that Devise uses flash messages to let users know if sign in was successful or unsuccessful. Devise expects your application to call

1
flash[:notice]

and flash[:alert] as appropriate. Do not print the entire flash hash, print only specific keys. In some circumstances, Devise adds a :timedout key to the flash hash, which is not meant for display. Remove this key from the hash if you intend to print the entire hash.

Configuring routes

Devise also ships with default routes. If you need to customize them, you should probably be able to do it through the devisefor method. It accepts several options like :classname, :path_prefix and so on, including the possibility to change path names for I18n:

1
  devise_for :users, path: 'auth', path_names: { sign_in: 'login', sign_out: 'logout', password: 'secret', confirmation: 'verification', unlock: 'unblock', registration: 'register', sign_up: 'cmon_let_me_in' }

If you have the need for more deep customization, for instance to also allow “/signin” besides “/users/signin”, all you need to do is create your routes normally and wrap them in a devise_scope block in the router:

1
2
3
devise_scope :user do
  get 'sign_in', to: 'devise/sessions#new'
end

This way, you tell Devise to use the scope :user when “/signin” is accessed. Notice devisescope is also aliased as as in your router.

Please note: You will still need to add devise_for in your routes in order to use helper methods such as current_user .

1
  devise_for :users, skip: :all

Strong Parameters

When you customize your own views, you may end up adding new attributes to forms. Rails 4 moved the parameter sanitization from the model to the controller, causing Devise to handle this concern at the controller as well.

There are just three actions in Devise that allow any set of parameters to be passed down to the model, therefore requiring sanitization. Their names and default permitted parameters are:

  • sign_in (Devise::SessionsController#create) - Permits only the authentication keys (like email)
  • sign_up (Devise::RegistrationsController#create) - Permits authentication keys plus password and password_confirmation
  • account_update (Devise::RegistrationsController#update) - Permits authentication keys plus password, password_confirmation and current_password

In case you want to permit additional parameters (the lazy way), you can do so using a simple before filter in your ApplicationController:

1
2
3
4
5
6
7
8
9
  class ApplicationController < ActionController::Base
    before_action :configure_permitted_parameters, if: :devise_controller?

    protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
    end
  end

The above works for any additional fields where the parameters are simple scalar types. If you have nested attributes (say you’re using accepts_nested_attributes_for), then you will need to tell devise about those nestings and types:

1
2
3
4
5
6
7
8
9
  class ApplicationController < ActionController::Base
    before_action :configure_permitted_parameters, if: :devise_controller?

    protected

    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, address_attributes: [:country, :state, :city, :area, :postal_code]])
    end
  end