Adding a New Strategy to OmniAuth

Javier Allegue Barros

Recently I had to authenticate with Rdio, a service that OmniAuth didn’t support. I’d never created an authentication strategy for OmniAuth before, so here’s how I did it and what I learned.

OmniAuth supports multiple different authentication schemes, but Rdio thankfully uses OAuth. Each authentication scheme has a separate gem/directory, so I looked in oa-oauth/lib/omniauth/strategies to see how other OAuth strategies had been implemented. OAuth has a few different versions now, so make sure you’re looking at examples that use the same OAuth version as the API you’re trying to connect to supports. Rdio uses OAuth 1.0, so those are the strategies I looked at.

I started by copying the module/class structure from another strategy into a new file and renaming the class where appropriate.

# oa-oauth/lib/omniauth/strategies/rdio.rb
require 'omniauth/oauth'
require 'multi_json'

module OmniAuth
  module Strategies
    #
    # Authenticate to Rdio via OAuth and retrieve basic user information.
    # Usage:
    #    use OmniAuth::Strategies::Rdio, 'consumerkey', 'consumersecret'
    #
    class Rdio < OmniAuth::Strategies::OAuth

    end
  end
end

Then I started looking for other similarities among the various strategies. There’s a fair bit of variance in which methods are implemented and the naming of variables, but the first method that all strategies had in common was initialize.

# oa-oauth/lib/omniauth/strategies/rdio.rb
def initialize(app, consumer_key = nil, consumer_secret = nil, options = {}, &block)
  opts = {
    :site               => "http://api.rdio.com",
    :request_token_path => "/oauth/request_token",
    :access_token_path  => "/oauth/access_token",
    :authorize_url      => "https://www.rdio.com/oauth/authorize"
  }
  super(app, :rdio, consumer_key, consumer_secret, opts, options, &block)
end

I was lucky and the paths were very clearly listed in the API docs. All I did here was switch out the paths and change the second argument passed to super to :rdio.

The next common method was auth_hash. The auth_hash is the data that is returned after you successfully authenticate with a service. It has info like the provider, the provider-specific id, name, username, etc.

# oa-oauth/lib/omniauth/strategies/rdio.rb
def auth_hash
  OmniAuth::Utils.deep_merge(super, {
    "uid"       => user_hash["key"],
    "user_info" => user_info,
    "extra"     => { "user_hash" => user_hash }
  })
end

def user_info
  user = user_hash
  {
    "nickname"   => user["username"],
    "first_name" => user["firstName"],
    "last_name"  => user["lastName"],
    "name"       => "#{user['firstName']} #{user['lastName']}"
  }
end

def user_hash
  @user_hash ||= MultiJson.decode(@access_token.post("http://api.rdio.com/1/", {
    :method => "currentUser",
    :extras => "username"
  }).body)["result"]
end

This part required a bit of trial and error, because although the docs indicate what fields are required, they don’t say which are returned automatically and which have to be retrieved from the provider’s API. Eventually I figured out that provider and credentials are automatically set, everything else has to be retrieved from the API. Look in the API docs for the specific request that will return information about the current user. Hopefully the docs are good and/or they have an API explorer so you can play around with it and see exactly what data is returned and in what format it’s in.

Once you’ve got that figured out, map the fields returned to their appropriate values in the auth_hash. A fairly common pattern is to also just stick the whole hash returned under extra, in case there were some extra fields that couldn’t be mapped.

There are several other methods that some strategies implement, but after more trial and error it seems like the only two methods that absolutely have to be implemented are initialize and auth_hash.

Now make sure your new strategy is loaded.

# oa-oauth/lib/omniauth/oauth.rb
require "omniauth/core"

module OmniAuth
  module Strategies
    ...

    autoload :Rdio, "omniauth/strategies/rdio"
  end
end

Create a test for it.

# oa-oauth/spec/omniauth/strategies/rdio_spec.rb
require File.expand_path("../../../spec_helper", __FILE__)

describe OmniAuth::Strategies::Rdio do
  it_should_behave_like "an oauth strategy"
end

Update the readme.

# README.markdown
* Rdio (via [brandonweiss](http://github.com/brandonweiss))

And you’re done. Now you’ll probably want to try it out to make sure it works. The easiest way is to open up the project that’s going to use the new strategy and edit the Gemfile to look for omniauth locally.

# Gemfile
gem "omniauth", path: "~/Code/omniauth"

bundle install then start the server and you should be good to go. If you messed something up, fix it, then restart the server to pick up the changes.

OmniAuth is a really great gem. Hopefully this will make it easier for people to add more strategies.