Skip to content

Strategy Contribution Guide

Ryan Wold edited this page Sep 7, 2019 · 22 revisions

Note: This document is now aimed at OmniAuth 1.0 and as such should not be assumed to be accurate for versions 0.3 and prior.

So you want to build an OmniAuth strategy...

Awesome! It's actually really quite easy to do, but can be a daunting task for people who aren't as familiar with the OmniAuth code. We're going to step through the creation of a basic strategy that will cover most of the parts of the OmniAuth core classes and help you understand the authentication lifecycle as well as all of the little conveniences that OmniAuth has to offer.

Our goal will be to walk through the construction of the Developer strategy included with OmniAuth as both an example and a handy tool for times when you might want to worry about the real-world strategies "later." The developer strategy is essentially a "fake" strategy, one that simply pipes user input straight back out the other side to create "users" that have no database or external service backing them up. This is obviously something that should never be used in a production environment, but serves as a useful kickstart if your authentication has some complexities that you'd rather not deal with at the outset.

All OmniAuth strategies are simply Ruby classes that include the OmniAuth::Strategy mixin module. This is a heavyweight module that adds quite a bit of functionality to your class, so while it is possible to use this on an existing class, the common use case will be including it in a bare class (think of it as being similar to MongoMapper, DataMapper and other include-based ORMs).

So for our developer strategy, that's how we'll start: by defining the class and including the proper module. If we define our class in the OmniAuth::Strategies namespace that will allow it to be declared by symbolic name instead of class in an OmniAuth::Builder call (more on that later). So let's get started:

require 'omniauth'

module OmniAuth
  module Strategies
    class Developer
      include OmniAuth::Strategy
    end
  end
end

OmniAuth strategies can essentially be broken into a number of "phases" that happen sequentially to authenticate a user and garner any necessary information. These steps are:

  1. The Setup Phase: This optional phase allows developers to configure a strategy dynamically at runtime. You should never assume when creating a strategy that it will be static (i.e. don't use class variables or other mechanisms that can't be safely changed on a request-to-request basis). Luckily, you don't need to do anything to enable this phase since OmniAuth takes care of it for you.
  2. The Request Phase: This can be considered an "information gathering" phase where the end user is asked to provide information or redirected to an external site to authenticate. You will be implementing this phase by overriding the request_phase method in the strategy (we'll cover that in the next section).
  3. The Callback Phase: This phase is used to take the information gathered in the request phase, transform it into the standardized schema required by OmniAuth, and then present it back to the underlying application. OmniAuth has some convenience methods to help this phase go as smoothly as possible.

And that's it! Every strategy must have a request and callback phase to function properly, and the setup phase is taken care of automatically, so you really only have two things to worry about when constructing your strategy. Lucky you!

As of OmniAuth 1.0 you are strongly encouraged to store any and all configuration for your strategy directly in the Options object provided to you by OmniAuth. This will provide maximum consistency across strategies for end users as well as allowing you to use the simple but powerful built-in tools like declarative configuration. For our Developer strategy, for instance, why don't we make it possible to configure which OmniAuth fields will be asked of the authenticating user, and default it to only requiring an E-Mail and a Name.

module OmniAuth
  module Strategies
    class Developer
      include OmniAuth::Strategy

      option :fields, [:name, :email]
      option :uid_field, :email
    end
  end
end

What we've just done is declared two default options for our strategy. The first says that we want a :name and :email from the user by default, the second says that we will use :email as a fake unique identifier for the purposes of this developer strategy.

Now, if I initialize the strategy, these values will automatically be set. Additionally, however, I get the added benefit of being able to override them at initialization by default. Let's see how that works:

app = lambda{|env| [200, {}, ["Hello World."]]}
OmniAuth::Strategies::Developer.new(app).options.uid_field                      # => :email
OmniAuth::Strategies::Developer.new(app, :uid_field => :name).options.uid_field # => :name

Not only are the declared options configurable upon instantiation of the strategy, but they are also inherited by subclasses! This means that creating specific strategies for a generic template such as OAuth will become far simpler in the future.

The request phase of an OmniAuth strategy will usually either redirect the user to a third-party site (for strategies such as OAuth, OAuth2) or prompt the user for more information (such as LDAP), or sometimes both (such as OpenID). If it's a redirect you need, it's very simple to build. Here's OAuth2's request_phase:

def request_phase
  redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(options.authorize_params))
end

Just return the redirect method called with the URI of your choice and OmniAuth will handle the rest. If you need to prompt the user for additional information, however, you will likely want to use OmniAuth::Form.

OmniAuth::Form

OmniAuth has a very simple form builder built into it so that you can easily prompt the user for information that's needed for your strategy. These forms are really not meant to be shown to end users but rather work as a stopgap for developers until they implement custom forms or other forms of indirection to get to the auth endpoints. So let's build the request phase for our Developer strategy:

def request_phase
  form = OmniAuth::Form.new(:title => "User Info", :url => callback_path)
  options.fields.each do |field|
    form.text_field field.to_s.capitalize.gsub("_", " "), field.to_s
  end
  form.button "Sign In"
  form.to_response
end

In this request phase, we've created a new form that will prompt the user with a title of "User Info" that will POST through directly to the strategy's callback path. We can do this since we're not actually relying on an external authentication provider for this strategy. The form will prompt the user for each of the fields specified in the strategy configuration.

In OmniAuth, you don't necessarily need to define a callback_phase method. The goal of the callback phase is simply to set omniauth.auth to an AuthHash and then call through to the app. By default, OmniAuth will take care of this for you by exposing some DSL methods:

  • uid: this is a unique string identifier that is globally unique to the provider you're writing
  • info: this method should return a hash of information about the user with keys taken from the Auth Hash Schema
  • credentials: if your strategy has a by-product of some kind of API credentials, this should return a hash with them included. Default keys to be used are 'token' and 'secret' (if that maps to your strategy).
  • extra: this is where you can store extra information that might be useful to someone using your strategy but doesn't fit anywhere else. Examples could be the raw user hash from an API call, an access token object for API access, or other things.

For our simple Developer strategy, this is easy! Since we're posting directly to the callback phase and simply passing through posted information, we can just define methods to construct information based on the passed parameters:

uid do
  request.params[options.uid_field.to_s]
end

info do
  options.fields.inject({}) do |hash, field|
    hash[field] = request.params[field.to_s]
    hash
  end
end

What we've done here is first defined the UID as the field specified in the :uid_field option passed on initialization. Next, we create a hash and set each of the fields defined in the :fields to a key on the info hash. That's all we have to do!

Fetching External Data

For the vast majority of providers, you may need to make some kind of API call to an external resource to populate the user info hash. Here are a few things to keep in mind:

  1. If you possibly can, write your strategy such that the uid method is not dependent upon a second API call. This may not be possible in all circumstances, but will allow users to make more efficient apps by passing a :skip_info option to the strategy which will avoid calling the info method if a uid is already in the system, for example.
  2. As much as possible, try to avoid using external API library gems to fetch user info. While it may be convenient, it simply adds additional dependencies to the strategy when OmniAuth strategies should be focused purely on authentication, not API access.
  3. The "default stack" for external API fetching and response parsing should use one of the following gems. Please don't use other gems unless your strategy has a compelling reason to do so:

When you are ready to release your strategy for others to use, the best way to do so is by packaging it as a Ruby gem. One of the easiest ways to build a gem is with Bundler. That's how OmniAuth is packaged and might be a good solution for your strategy, as well. Some things to keep in mind when creating your gem:

  • The gem's name should be omniauth-your-strategy where your-strategy is the same as the file name your strategy inhabits with dashes instead of underscores.
  • You should provide a lib/omniauth-your-strategy.rb file that will load your strategy and make it ready to use, including requiring OmniAuth and any other dependencies.
  • It is recommended that you put your strategy in the location lib/omniauth/strategies/your_strategy.rb.
  • If your strategy has a filename that can't be turned into a class through simple substitution (for example oauth should camelize to OAuth not Oauth) you will need to add a custom camelization rule to OmniAuth like so: OmniAuth.config.add_camelization('oauth', 'OAuth'). This will make your strategy usable in OmniAuth::Builder via the provider :your_strategy convenience implementation.
  • OmniAuth follows semantic versioning, so you can depend on any release in the major version you're developing off of (for example, `gem.add_dependency 'omniauth', '~> 1.0').
  • As stated above, it is strongly preferred that you use faraday for an HTTP client, multi_json for JSON parsing/encoding and multi_xml for XML parsing/encoding.
  • You can use the omniauth-test-harness, a simple Ruby on Rails 4 application, to test the strategy.

Of course a great way to see how strategies are packaged up is to look at existing gems (for example, omniauth-oauth or omniauth-identity).