The Magical Devise Journey - Limiting Warden Strategies on specific controllers.
Today I was solving how to skip particular Devise strategies on specific controllers. So here’s the thing, we use Devise and recently added a Doorkeeper setup for JWT API Authentication. This is all very nice, but the more strategies you add the more overhead there is when authenticating. 6 strategies when you know you’re only going to need one is too much. But perhaps the biggest problem is if you’re logged in with a session cookie, API responses work out of the box for the current authenticated user and we want/need our public facing API to be stateless.
This begs the question how does all this magic work anyway. Well, Devise is built on Warden, a Rack middleware. You define strategies in your User model and this in turn indirectly configures a Warden::Proxy object which is stored in the Request#env of your incoming request. I assume this is just flexible and can be accessed inside/outside of controllers in middleware, etc. When your controller needs the current user it queries the Warden::Proxy and all of the prefined strategies are consulted in turn to find it.
You can see this here in https://github.com/heartcombo/devise/blob/master/lib/devise/controllers/helpers.rb
module Devise
module Controllers
module Helpers
module ClassMethods
def self.define_helpers(mapping) #:nodoc:
mapping = mapping.name
class_eval <<-METHODS, __FILE__, __LINE__ + 1
def authenticate_#{mapping}!(opts = {})
opts[:scope] = :#{mapping}
warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end
def #{mapping}_signed_in?
!!current_#{mapping}
end
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
def #{mapping}_session
current_#{mapping} && warden.session(:#{mapping})
end
METHODS
ActiveSupport.on_load(:action_controller) do
if respond_to?(:helper_method)
helper_method "current_#{mapping}", "#{mapping}_signed_in?", "#{mapping}_session"
end
end
end
# The main accessor for the warden proxy instance
def warden
request.env['warden'] or raise MissingWarden
end
end
end
end
end
You give Warden a scope and it will find your User or not.
def current_user
warden.authenticate(scope: :user)
end
However, this is consulting every strategy and in our API base controller we only care about the JWT strategy. Warden allows you to query all or specific strategies (https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb)
module Warden
class Proxy
# Example:
# env['warden'].authenticate(:password, :basic, :scope => :sudo)
#
# :api: public
def authenticate(*args)
user, _opts = _perform_authentication(*args)
user
end
end
end
We can overwrite the authenticate_user!, current_user methods in our API base controller.
def authenticate_user!(opts = {})
opts[:scope] = :user
warden.authenticate!(:jwt, opts) if !devise_controller? || opts.delete(:force)
end
def current_user
@current_user ||= warden.authenticate(:jwt, scope: :user)
end
Or to be more flexible
class ApplicationController < ActionController::Base
class_attribute :warden_strategies
def authenticate_user!(opts = {})
opts[:scope] = :user
warden.authenticate!(*self.class.warden_strategies, opts) if !devise_controller? || opts.delete(:force)
end
def current_user
@current_user ||= warden.authenticate(*self.class.warden_strategies, scope: :user)
end
end
class ApiController < ApplicationController
self.warden_strategies = :jwt
end
Yay, stateless API requests. Phew.
I gone did a Pull Request in case it is valuable to anyone else https://github.com/heartcombo/devise/pull/5392/files