GenomeSpace OpenID Authentication for Ruby on Rails with Devise

For those of you that are Ruby on Rails developers, you're most likely already familiar with Devise. It's a fantastically simple and flexible way to add password- and token-based authentication to your Rails application.  What you may not know is that there is a companion gem called 'devise_openid_authenticatable' that extends Devise's authentication strategies to include OpenID.  This post will show you how you can implement this to authenticate your application against the GenomeSpace OpenID server.

Setup

For this example, we're using Rails 3.2.12 and Ruby 1.9.2. You can use newer versions of Ruby, but I don't recommend using Rails 4 for this - there are issues with OpenID and the CSRF authenticity token in Rails that causes authentication to fail unless you completely disable it, which is less than ideal if this application is going to be public.

Once you've created your application, you'll need to add these 3 gems to your Gemfile:

gem 'devise'
gem 'devise_openid_authenticatable'
gem 'warden'

After that, create a User model in the standard fashion (see the Devise documentation for more information).  If this is a new application you've created, you also need to set up a simple controller with an index method to give Devise somewhere to redirect to once we've authenticated.  Here's a quick example:

From the command line:

rails g controller home index

Add this line to app/controllers/home_controller.rb

before_filter :authenticate_user!

Finally, edit your config/routes.rb:

devise_for :users
get "home/index"
root to: "home#index"

It would also be a good idea at this point to read our OpenID Requirements documentation as well so that you're familiar with the concepts before moving forward.

The User Model

Once you've created your user model, you're going to want to edit it so that it only tries to authenticate against OpenID. To do this:

  1. Remove the :encrypted_password column from your User model and add :username:token and :identity_url fields.  The easiest way to do this is via migrations (see below for more details).
  2. Change your devise declaration to be :openid_authenticatable instead of :database_authenticatable or :token_authenticatable.  Other Devise properties can configured as you like (such as :trackable:rememberable, etc.)
  3. Set up your attr_accessible declaration to include these new fields: attr_accessible :identity_url, :email, :username, :token

1. Migration to add necessary fields:

class AddOpenIDFieldsToUsers < ActiveRecord::Migration
  def up
    add_column :users, :identity_url, :string
    add_column :users, :username, :string 
    add_column :users, :token, :string
    remove_column :users, :encrypted_password 
  end
  def down
    remove_column :users, :identity_url
    remove_column :users, :username
    remove_column :users, :token
    add_column :users, :encrypted_password, :string
  end
end

2, 3. Edits to app/models/user.rb

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :token_authenticatable, :database_authenticatable, :confirmable, :lockable, :timeoutable, :omniauthable, 
  # :registerable, :recoverable, :rememberable, :trackable, :validatable         
  devise  :openid_authenticatable, :trackable
		
  # Setup accessible (or protected) attributes for your model
  attr_accessible :identity_url, :email, :username, :token

GenomeSpace OpenID Parameters & Forms

The next thing that you'll need to do is set up the proper GenomeSpace OpenID parameters inside you User model.  This is done via a custom extension (option #3 from our OpenID Requirements documentation).  To set this up, you need to do 2 things:

  1. Render the standard Devise views and edit the devise/sessions/new.html.erb form to use GenomeSpace's OpenID URL (obtained here: http://www.genomespace.org/sites/genomespacefiles/config/serverurl.properties).  You can render all of Devise's forms with this command: rails g devise:views
  2. Implement the following methods inside your User model
    1. self.openid_required_fields
    2. self.build_from_identity_url(identity_url)
    3. openid_fields=(fields)

1. app/views/devise/sessions/new.html.erb:

<%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %>
  <%= f.hidden_field :identity_url, value: "https://identity.genomespace.org/identityServer/xrd.jsp" %>
  <%= f.submit "Sign in using GenomeSpace" %>
<% end %>

In our login form (above), you can see that we've removed the standard username/password fields and instead added a hidden field that points directly at GenomeSpace's OpenID URL.  This way, when the user clicks the "Login with GenomeSpace" button, they are redirected to our GenomeSpace login page, which will then return back to your Rails application once the login is successfully completed.

2. Implement required OpenID User methods (inside app/models/user.rb):

def self.openid_required_fields
  ["gender", "nickname", "email"]
end 
  
def self.build_from_identity_url(identity_url)
  User.new(:identity_url => identity_url)
end
  
def openid_fields=(fields)
  fields.each do |key, value|
    # Some AX providers can return multiple values per key
    if value.is_a? Array
      value = value.first
    end
  
    case key.to_s
    when "nickname"
      self.username = value
    when "email"
      self.email = value
    when "gender"
      self.token = value
    else
      logger.error "Unknown OpenID field: #{key}"
    end
  end
end 

These 3 methods will provide the necessary functionality so the devise_openid_authenticatable can map the fields from GenomeSpace's OpenID server to the instance of the User model you're working with.  Here's a brief overview of what each method is doing:

i. self.openid_required_fields

This is an array of required fields for any successful authentication response.  We're providing the SReg names for fields here even though we're using the custom extension as this is what is returned from GenomeSpace (more on this later).  Specifically, we're saying that GenomeSpace must provide the gender and nickname fields in it's response.  We'll map these fields to our User model later.

ii. self.build_from_identity_url(identity_url)

This method automatically creates your User record once we've successfully logged in and records it in the database.  The :identity_url is your GenomeSpace OpenID unique identifier, and takes the following format: https://identity.genomespace.org/identityServer/xrd/[YOUR GENOMESPACE USERNAME].  We need this method so that we can record our other OpenID fields somewhere later on.

iii. openid_fields=(fields)

This is where the real heavy lifting happens.  In this instance method (as opposed to the above class methods), once we have a valid User instance, we can then take the response from OpenID and parse out the various fields that we said we needed.  It takes a hash from devise_openid_authenticatable (fields) and maps each field to what we want.  Specifically, it takes the SReg names for the values we requested and maps them inside the case statement to the User columns we created earlier.

Handling the Custom Extension Response

You might think you're done at this point, but there is one last thing we need to do.  Even though we're using the custom extension, devise_openid_authenticatable can only send the request as either SimpleRegistration (SReg) or AttributeExchange (AX).  This was why we specified the SReg names for the parameters in our required fields rather than the custom GenomeSpace fields.  We need this because otherwise the fields aren't "signed" when they're returned, and devise_openid_authenticatable won't parse any unsigned fields.  So we need to tell devise_openid_authenticatable how to handle this custom case.  It requires a one-line fix to the source code for the gem.  To do this:

  1. Vendor all of your gems for your app with: bundle install --deployment
  2. Edit the fields method inside strategy.rb (located at vendor/gems/.../devise_openid_authenticatable/lib/devise_openid_authenticatable/strategy.rb)

Once you've vendored all of your gems (it is possible to only vendor the devise_openid_authenticatable gem, but I don't recommend it - it's much simpler to vendor them all), find the strategy.rb file inside the lib/devise_openid_authenticatable folder of the gem.  Navigate down to the fields method (it should be around line 100) and look for the following line:

if ns_alias.to_s == "sreg"

And change the line to read like so:

if ns_alias.to_s == "sreg" || ns_alias.to_s == "ext1"

The reason we're doing this again is because of the response that comes back from GenomeSpace.  If you refer back to the GenomeSpace OpenID Requirements documentation, you'll notice that using the custom extension sets the namespace to "ext1" instead of "sreg".  Since the fields method parses the values returned by from OpenID by namespace, if we don't tell it how to handle this special namespace your username, email and token values won't get recorded.

That's it - you've now got a simple Rails application that can authenticate against GenomeSpace's OpenID server.