Monday, September 05, 2016

Rails: Custom Authentication Strategies With Devise and Warden


You're probably familiar with Devise. Devise is a flexible authentication solution for Rails based on Warden

Warden uses strategies to try to authenticate sessions. However, it provides no strategy, you have to implement them yourself. Devise implements a few that you may already know.

  • Devise Model Module
Models are more commonly known as Database Authenticatable, Rememberable, Trackable, Confirmable, etc.
When you install and bootstrap Devise in your project you should see something like this in the model User:

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


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


require 'devise/strategies/custom_authenticatable'

module Devise
  def self.bcrypt(klass, password)
    ActiveSupport::Deprecation.warn "Devise.bcrypt is deprecated; use Devise::Encryptor.digest instead"
    Devise::Encryptor.digest(klass, password)
  end

  module Models
    # Authenticatable Module, responsible for hashing the password and
    # validating the authenticity of a user while signing in.
    #
    # == Options
    #
    # DatabaseAuthenticatable adds the following options to devise_for:
    #
    #   * +pepper+: a random string used to provide a more secure hash. Use
    #     `rake secret` to generate new keys.
    #
    #   * +stretches+: the cost given to bcrypt.
    #
    # == Examples
    #
    #    User.find(1).valid_password?('password123')         # returns true/false
    #
    module CustomAuthenticatable
      extend ActiveSupport::Concern

      included do
        after_update :send_password_change_notification, if: :send_password_change_notification?

        attr_reader :password, :current_password
        attr_accessor :password_confirmation
      end

      def self.required_fields(klass)
        [:encrypted_password] + klass.authentication_keys
      end

      # Generates a hashed password based on the given value.
      # For legacy reasons, we use `encrypted_password` to store
      # the hashed password.
      def password=(new_password)
        @password = new_password
        self.encrypted_password = password_digest(@password) if @password.present?
      end

      # Verifies whether a password (ie from sign in) is the user password.
      def valid_password?(password)
        Devise::Encryptor.compare(self.class, encrypted_password, password)
      end

      # Set password and password confirmation to nil
      def clean_up_passwords
        self.password = self.password_confirmation = nil
      end

      # Update record attributes when :current_password matches, otherwise
      # returns error on :current_password.
      #
      # This method also rejects the password field if it is blank (allowing
      # users to change relevant information like the e-mail without changing
      # their password). In case the password field is rejected, the confirmation
      # is also rejected as long as it is also blank.
      def update_with_password(params, *options)
        current_password = params.delete(:current_password)

        if params[:password].blank?
          params.delete(:password)
          params.delete(:password_confirmation) if params[:password_confirmation].blank?
        end

        result = if valid_password?(current_password)
                   update_attributes(params, *options)
                 else
                   self.assign_attributes(params, *options)
                   self.valid?
                   self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
                   false
                 end

        clean_up_passwords
        result
      end

      # Updates record attributes without asking for the current password.
      # Never allows a change to the current password. If you are using this
      # method, you should probably override this method to protect other
      # attributes you would not like to be updated without a password.
      #
      # Example:
      #
      #   def update_without_password(params, *options)
      #     params.delete(:email)
      #     super(params)
      #   end
      #
      def update_without_password(params, *options)
        params.delete(:password)
        params.delete(:password_confirmation)

        result = update_attributes(params, *options)
        clean_up_passwords
        result
      end

      # Destroy record when :current_password matches, otherwise returns
      # error on :current_password. It also automatically rejects
      # :current_password if it is blank.
      def destroy_with_password(current_password)
        result = if valid_password?(current_password)
                   destroy
                 else
                   self.valid?
                   self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
                   false
                 end

        result
      end

      # A callback initiated after successfully authenticating. This can be
      # used to insert your own logic that is only run after the user successfully
      # authenticates.
      #
      # Example:
      #
      #   def after_database_authentication
      #     self.update_attribute(:invite_code, nil)
      #   end
      #
      def after_database_authentication
      end

      # A reliable way to expose the salt regardless of the implementation.
      def authenticatable_salt
        encrypted_password[0,29] if encrypted_password
      end

      def send_password_change_notification
        send_devise_notification(:password_change)
      end

      protected

      # Hashes the password using bcrypt. Custom hash functions should override
      # this method to apply their own algorithm.
      #
      # See https://github.com/plataformatec/devise-encryptable for examples
      # of other hashing engines.
      def password_digest(password)
        Devise::Encryptor.digest(self.class, password)
      end

      def send_password_change_notification?
        self.class.send_password_change_notification && encrypted_password_changed?
      end

      module ClassMethods
        Devise::Models.config(self, :pepper, :stretches, :send_password_change_notification)

        # We assume this method already gets the sanitized values from the
        # DatabaseAuthenticatable strategy. If you are using this method on
        # your own, be sure to sanitize the conditions hash to only include
        # the proper fields.
        def find_for_database_authentication(conditions)
          find_for_authentication(conditions)
        end
      end
    end
  end
end



Next we need to register our Model with Devise. We use Devise#add_module to do this. We can do this inside the file devise.rb that you can see in the folder config/initializers. Just add it to the top of the file.


Devise.add_module(:custom_authenticatable, {
   strategy: true,
   controller: :sessions,
   model: 'devise/models/custom_authenticatable',
   route: :session,
})


Without all of those options, Devise and Rails’ Router won’t know to delegate Requests to SessionController into your Module.

  • Warden Strategy
A strategy is a place where you can put logic related to authentication. Any strategy inherits from Warden::Strategies::Base.

The Warden::Strategies.add method is a simple way to provide custom strategies. You must declare an authenticate! method. You may provide a valid? method. The valid method should return true or false depending on if the strategy is a valid one for the request.




require 'devise/strategies/authenticatable'

module Devise
  module Strategies
    class CustomAuthenticatable < Authenticatable


      def authenticate!
        resource  = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
        hashed = false
        if validate(resource){ hashed = true; resource.valid_password?(password) && resource.role.is_active? }
          remember_me(resource)
          resource.after_database_authentication
          success!(resource)
        end

        mapping.to.new.password = password if !hashed && Devise.paranoid
        fail(:not_found_in_database) unless resource
      end


    end
  end
end

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


In this scenario, I need to verify that user is active or not. So I created my own Strategy and I added the necessary logic for this project.  Inactive users can not do log in.

One important thing. In order to use my new authentication strategy, you must modify your User model and add something like this:

class User < ActiveRecord::Base
    devise :custom_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable
  end

No comments:

Post a Comment