“...I've been working since 2008 with Ruby / Ruby on Rails, love a bit of Elixir / Phoenix and learning Rust. I also poke through other people's code and make PRs for OpenSource Ruby projects that sometimes make it. Currently working for InPay...”

Rob Lacey
Senior Software Engineer, UK

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

GPK of the Day Wrappin' RUTH