If you’ve built many Rails application, odds are high you’ve encountered the Devise gem. Devise provides several standard features we have all come to expect from modern web applications, such as logging in, securely storing passwords in a database, user tracking, and automatic session expiration. However, when you must authenticate with a new service or your company has its own authentication mechanisms (for example, an SSO portal) do avoid the temptation to design your own authentication service. Instead, follow the steps in this guide and you’ll be able to use Devise and Warden with any form of authentication.

Before I wrote this article, I searched high and low on Stack Overflow and the internet in general for guides on how to do this. I came across the Devise LDAP Authentication wiki, and several articles and demo apps describing how to do this, but all of these links were light on the details and didn’t spell out in plain terms the interaction between your Rails app, Devise, Warden, and your custom authentication classes. Furthermore, most articles were out of date and referenced obsolete version of Devise and Rails. As a result, I struggled a good bit before I finally connected all the dots and got my own custom Devise authentication strategy working. In this article, I am going to show you how to do this too using Rails 4.2 and Devise 4.1 and discuss a little more about whats going on under the hood towards the end. In the end, I hope to impart a better understanding of how Devise and Warden work together with Rails!

Components of a Custom Devise Authentication Implementation

Summing up everything that I read above, Devise and Warden interact with your Rails app through Models and Strategies. Models are modules that get mixed into any Ruby class that declares devise. Strategies are Ruby classes that are registered with Warden, must define an instance method named authorize!, and must be a subclass of Warden::Strategies::Base. Once Warden knows about your Strategy class and Devise knows about your module, the two will work together to allow you to authenticate the resource (or not!) and control what information is exposed to your controller.

Devise Model Module

Models are more commonly known as Database Authenticatable, Rememberable, Trackable, etc.

You may have seen something like this when bootstrapping Devise:

  #example User class using Devise
  class User < ActiveRecord::Base
    devise :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable
  end

All of those symbols have been registered with Devise (more on this later!) so when this model is loaded and devise is invoked, the Models contained inside of the Devise Gem are mixed into the devise class. Any methods defined in those Model modules become available on the Devise model on which is was defined. If this process is not familiar to you, read up on Ruby Modules and mixins.

Since we want to write our own Authentication Implementation, we must follow suit and write a Model, too.

  require Rails.root.join('lib/devise/strategies/custom_authenticatable')

  module Devise
    module Models
      module CustomAuthenticatable
        extend ActiveSupport::Concern

        included do
          attr_accessor :custom_user_groups
        end

        class_methods do

          # defining a class method that can find or create a Resource record and returns it back
          # to our Authentication Strategy
          # 
          # If a user needs to sign up first, 
          #   with Registerable, merely look up the record in your database 
          #   instead of creating a new one

          def find_or_create_with_authentication_profile(profile)
            resource = self.where(username: profile.user_id).
              first_or_create({
                email: profile.e_mail_address,
                first_name: profile.first_name,
                last_name: profile.last_name,
              })
            resource.custom_user_groups = profile.user_groups
            resource
          end

          ####################################
          # Overriden methods from Devise::Models::Authenticatable
          ####################################

          # This method takes as many arguments as there are elements in `serialize_into_session`
          #
          # It recreates a resource from session data
          #
          def serialize_from_session(id, custom_user_groups)
            #Lookup the record with the Primary Key
            resource = find(id)

            #Assign any additional attributes
            resource.custom_user_groups = custom_user_groups
            
            #Return the Resource
            resource
          end

          # Serialize any data you want into the Session.  The Resources Primary Key or other Unique Identifier
          # is recommended.
          # 
          # The items placed into this array must be Serializable, e.g. numbers, strings, symbols, and Hashes.
          # Do not place entire Ruby Objects or Arrays of Objects into the Session.
          #
          def serialize_into_session(resource)
            [resource.id, resource.custom_user_groups]
          end
        end
      end
    end
  end

Devise automatically exposes current_<resource> in your controllers - so anything that you serialize/deserialize in and out of the session can be used in your controllers via that accessor method. In a specific implementation of this with one of my clients, we needed to pass an array of User Groups (permissions) from the SSO Service into the Rails app. I did this by storing them in the session and creating a virtual attribute on the Devise model (see custom_user_groups above.

Devise add_module hook.

Next we need to register our Model with Devise. We use Devise#add_module to do this.

  Devise.add_module(:custom_authenticatable, {
    strategy: true,
    controller: :sessions,
    model: 'devise/models/custom_authenticatable', #<- same string you'd use to `require` this model
    route: :session,
  })

This particular line of code took me a while to track down, its generally telling Devise to add a module called “custom_authenticatable”, its carries a Warden Strategy with it (below!), it is mapped to Devise’s SessionController, it requires the model we created above, and it can create routes. Without all of those options, Devise and Rails’ Router won’t know to delegate Requests to SessionController into your Module.

You can put this anywhere you’d like. To keep things simple, I just added it to the top of config/initializers/devise.rb. Its Devise configuration afterall.

Warden Strategy Class

Strategies are Ruby classes that are registered with Warden, must define an instance method named authorize!, and must be a subclass of Warden::Strategies::Base. Luckily, Devise provides one for us called Devise::Strategies::Authenticatable so we’ll subclass our own Devise Strategy from Devise’s built in one. Once Warden knows about your Strategy class and Devise knows about your module, the two will work together to allow you to authenticate the resource (or not!) and control what information is exposed to your controller.

Lets take a look at a sample Strategy.

  require 'devise/strategies/authenticatable'

  module Devise
    module Strategies
      class CustomAuthenticatable < Authenticatable

        # Store response from remote authentication service, such as an SSO Token or API Key
        attr_accessor :sso_token

        # All Strategies must define this method.
        def authenticate!
          if password.present? && has_valid_credentials?
            resource = mapping.to.find_or_create_with_authentication_profile(get_authentication_profile)
            success!(resource)
          else
            fail(:unable_to_authenticate)
          end
        end

        ## returns true or false if the given user is found on MyPassport
        def has_valid_credentials?
          #Implement logic that returns a True or False if the user's credentials are correct.
          #We'll generate a resource later, but first we need to know we're dealing with a legit request.
        end

        def get_authentication_profile
          #returns some data about the user that was in the response from the Remote Authentication Service.
          #Most SSO Systems will return back a user's UniqueID, Email Address, and other attributes that can be used
          # to further refine access to resources in your Rails app.
        end
      end
    end
  end

  Warden::Strategies.add(:custom_authenticatable, Devise::Strategies::CustomAuthenticatable)

Registering your Strategy with Warden

This is straightforward. Its the last line of the Strategy class and follows suit with all the other Warden Strategies I found.

  Warden::Strategies.add(:custom_authenticatable, Devise::Strategies::CustomAuthenticatable)

Devise::Strategies::Authenticatable#valid?

This one took me a while to figure out. Unless you are doing everything right the first time and passing data into Devise’s SessionController just so, you may think that Devise just isn’t using your Strategy at all. On the contrary, its loaded - you just aren’t sending in a valid request and Devise is preventing the request from validating through Warden. This is happening inside the parent class of the Strategy above.

TL;DR - Devise expects its Session Controller to receive authorization data with a nested structure:

  {
    user: { email: [email protected], password: 'abd123' }
  }

I recommend structuring your Requests in the way that Devise expects. If you must support other query parameter structures, you can, but breaking these conventions and customizing your Session Controller is outside the scope of this blog post (but entirely possible!)

Connecting it all together

Writing a Custom Devise Authentication Implementation might seem overly complicated, but the benefits of building your authentication system on top of Rails’ de-facto gem is well worth the effort. Considering you can use any of Devise’s other models along with your custom authorization means you can keep your code very DRY as well. I hope this guide will help others learn how to do this quickly and easily, because I felt like the existing guides, blog posts, and the rest were a bit tough to follow.

Need help with your Rails app or struggling with Devise? Feel free to contact us and lets start a conversation!