Warden - Rails Authentication

Warden is a simple and powerful authentication mechanism for rack based ruby applications. Using a rack middleware, it injects an object into the rack environment which can be used to authenticate incoming requests.

At its core, Warden uses 'manager', 'proxy', and 'strategy' abstractions to accomplish its goals. The library shines in its implementation of the strategy pattern -- allowing for imaginative and unplanned authentication mechanisms. The manager highlights the power and simplicity of web middlewares as it injects the authentication object into the rack environment. Finally the proxy serves as the authentication strategies' context running them lazily on demand.

The authors' chosen abstractions map well to the problem domain -- however even greater clarity and robustness in design can be achieved through extracting responsiblities of the proxy and manager and through a greater use of dependency injection.

Terms to Know

  • Strategy a pattern for when you have a clear task to accomplish, but more than one way to do it.
  • Virtual Proxy a pattern for delaying creation of an object until it is needed.
  • Rack Middlware a minimal interface for ruby webservers. Imagine the web request moves through a pipe and each section of pipe is a middleware.
  • Extensibility is the ease at which new implementations (how something is accomplished) can be introduced while maintaining the existing interface (what is accomplished)
  • Composition a recognition of distinct concepts that make up a larger whole. A classic example is the car composed of wheels, engine, brakes, and more.
  • Single Responsiblity Principle states an object should only have "one reason to change" decoupling the actors in the system.
  • Dependency Inversion highlights the benefit of depending on abstractions over concretions -- both for higher level and low level concerns. If both high level and low level concepts depend on abstract interfaces then high level concepts achieve greater independence from low level concerns.

The Strategy

Warden strategies are responsible for housing specific authentication logic such as checking a username and password against a database or checking for the presence of a "remember me" cookie. Warden does not come with any concrete strategies -- only a base class that defines the required api. The strategy class is the strongest element of the library due to the extensibility and composability it brings.

Valid strategies must only implement authenticate! and optionally valid?. Because the public api is only one method, developers can easily implement their own strategies to fit their authentication needs.

Snippet Source

  def _run_strategies_for(scope, args)
    # ... omitted code

  (strategies || args).each do |name|
    strategy = _fetch_strategy(name, scope)
    next unless strategy && !strategy.performed? && strategy.valid?

    strategy._run! # this calls the underlying authenticate!
  end

The strategy builds on the value of composition by recognizing that often a particular task must be accomplished by an actor in the system, but there are numerous ways to accomplish the task. Therefore the mechanism of accomplishing a task is separated and abstracted from the actor. In this way the actor loses knowledge (a good thing!) of how it accomplishes its task while still getting the job done.

The Manager

Warden's manager is responsible for managing the library's lifecycle from injecting the authentication object to handling failed requests. The strength of the middleware pattern should be noted as it creates a pipeline of logic which can modify a web request as it enters the web server and can modify the response as it leaves. Despite the elegance of the middleware pattern the manager does not feel as focused or general as it could be.

In my analysis I identified four responsibilities of the manager. It injects the authentication object, handles failed requests, runs the callback lifecycle, handles library configuration, and provides methods for user deserialization and serialization configuration.

The manager would benefit from a UserProxy responsible for configuration and implementing user session (de)serialization. It would also benefit from an UnauthenticatedResponse which would process unauthenticated requests routing them to a redirect, custom response, or the failure app. Through the addition of these classes, the manager would become solely responsible for coordinating the authentication logic amongst these objects.

Snippet Source

def call_failure_app(env, options = {})  
  if config.failure_app
    # Concrete Request
    options.merge!(attempted_path: ::Rack::Request.new(env).fullpath)
    env["warden.options"] = options
    # ... omitted code

    config.failure_app.call(env).to_a
  else
    # ... omitted code
  end
end  

In the manager the config, proxy, and rack request are all instantiated by the manager internally. As a result the manager is coupled to these specific objects. If instead these objects were injected at initialization of the manager or through a method call, the manager would no longer be concerned with the type of the object but only whether it can respond to the necessary messages.

Snippet Source

def call(env) # :nodoc:  
  # ... omitted code

  env['warden'] = Proxy.new(env, self) # Concrete Proxy
  result = catch(:warden) do
    @app.call(env) # Abstract app and env
  end

  # ... omitted code
end  

Despite the multiple responsibilities and concrete dependencies the manager does a great job with the injection of the app and env as abstractions--a middleware requirement.

The Proxy

The proxy is a virtual proxy to a number of objects most importantly the hash of authentication strategies. The laziness of the proxy is a very nice design choice, improving application performance, while introducing limited complexity in the code. However the proxy also suffers from numerous responsibilities and reliance on concretions.

The proxy does a lot and has access to a lot. It lookups the strategies, caches the strategies, runs the authentication strategies, provides access to warden errors, allows reading, writing, deletion of the user from the session, and manages authentication scopes. Many of these responsibilities are facilitated by the initialization of the proxy with the rack env, manager, and config stored in instance variables.

The proxy could benefit from an Authenticator that runs the strategies, a StrategyCache that lookups and caches the strategies, a UserProxy for higher level read, write, delete access to the user in the session, ScopeManager which would manage the authentication scopes and their config. The introduction of a few additional objects would do a lot to simplify the complexity of the class.

Snippet Source

# Abstract
def initialize(env, manager)  
  @env, @users, @winning_strategies, @locked = env, {}, {}, false
  @manager, @config = manager, manager.config.dup
  @strategies = Hash.new { |h,k| h[k] = {} }
  manager._run_callbacks(:on_request, self)
end

# Concrete
def errors  
  @env[ENV_WARDEN_ERRORS] ||= Errors.new
end

def session_serializer  
  @session_serializer ||= Warden::SessionSerializer.new(@env)
end  

The proxy is also dogged by a few concrete dependencies. The errors and session serializer could be injected making this code more extensible. However the proxy also benefits from the presence of abstract manager and env dependencies.

Close

As an everyday user of Devise I love the simple and extendable authentication mechanism Warden offers. Warden not only is easy to use, it is a fun read and provides excellent case studies in the strategy pattern, middleware pattern, and proxy. If you are interested in learning more I would encourage you to look at Devise's use of Warden. There you will see a mature implementation of the library with multiple sophisticated strategies.

Thank you to Steve Bussey for feedback on a draft of this post.

photo credit: London, England via photopin (license)

Show Comments