Last Updated: February 25, 2016
·
3.672K
· dennismonsewicz

Devise: Fast User Switching using a custom Authentication Strategy

Recently, my company needed a way to allow for users with a certain Access Control to log in (or impersonate) another user.

To be able to keep up with the original user, checking to see if the original user has access to the given user they would like to impersonate and signing in the user for impersonation, we came up with the following strategy for Warden.

# lib/sign_in_as.rb
require 'devise/strategies/base'

module SignInAs
  module RememberContributor
    extend ActiveSupport::Concern

    private

      def remember_contributor_id
        request.env['rack.session']['devise.remember_contributor_id']
      end

      def remember_contributor_id=(id)
        request.env['rack.session']['devise.remember_contributor_id'] = id
      end

      def remember_contributor_id?
        request.env['rack.session'] && request.env['rack.session']['devise.remember_contributor_id'].present?
      end

      def clear_remembered_contributor_id
        request.env['rack.session']['devise.remember_contributor_id'] = nil
      end
  end
end
# config/initializers/sign_in_as.rb

require 'devise/strategies/authenticatable'

module Devise
  module Strategies
    class SignInAs < Authenticatable

      include ::SignInAs::RememberContributor

      def valid?
        user = User.find_by_id(params[:id])

        if user.athlete?
          contributor_has_access? && (ability.can?(:create, user) || ability.can?(:update, user))
        else
          clear_remembered_contributor_id
          true
        end
      end

      def authenticate!
        resource = User.find_by_id params[:id]

        if resource
          success!(resource)
        else
          fail!("You do not have sufficient access to this account")
        end
      end

      private
        def contributor_has_access?
          contributor_user.school_admin? || contributor_user.athlete_contributor? || contributor_user.class.name.eql?("HighSchoolCoach")
        end

        def contributor_user
          User.find(remember_contributor_id)
        end

        def ability
          @ability ||= "::Abilities::#{contributor_user.class.name}Ability".constantize.new(contributor_user)
        end
    end
  end
end

Warden::Strategies.add(:sign_in_as, Devise::Strategies::SignInAs)
class SignInAsController < ApplicationController
  before_filter :authenticate_user!

  include SignInAs::RememberContributor

  def create
    # Let's remember the contributor ID for use in the Warden::Strategy
    self.remember_contributor_id = original_user.try(:id) || current_user.id
    # Sign out current user
    sign_out(current_user)

    # If original_user and original_user ID eql params[:id].to_i, log original_user back in
    if original_user && original_user.id == params[:id].to_i
      sign_in(:user, original_user)
      redirect_to user_root_path
    else
      # Else pass off request to custom Warden::Strategy
      handle_request
    end
  end

  private
    def handle_request
      # IF Warden autheticates using Devise::Strategies::SignInAs, redirect them to the correct path
      if env['warden'].authenticate(:sign_in_as)
        redirect_to user_root_path
      else
        # ELSE sign the contributor back into their account, and tell them they no have rights, they go home
        sign_in(:user, User.find(remember_contributor_id))
        redirect_to user_root_path, notice: "You do not have sufficient rights"
      end
    end
end