Josh Software

Multiple Applications with Devise, Omniauth and Single Sign On – Josh Software

josh ruby

Multiple Applications with Devise, Omniauth and Single Sign On – Josh Software

The best way to scale an application is to split the application business logic into different inter-communicable components. However, authenticating, authorizing and security raise concerns. OAuth comes to the rescue – and like a knight in shining armour – omniauth steals the show.

Omniauth is an awesome gem that allows you to authenticate using Open-Id based social networks. There are TONS of topics on this – the one I liked best was from RailsRumble. Devise was also integrating oauth and oauth2 into its authentication framwork when omniauth was released (in Oct, 2010). So, Devise dumped their oauth integration in their v1.2oauth branch and started integrating master with omniauth instead (:omniauthable Devise Module).

We wanted to solve these problems:

  • A single User Manager application (which will authenticate ALL users with different roles)
  • Different internal applications which talk to User Manager for authentication
  • User should be able to login/sign-up via Social Networks like Twitter and Facebook.
  • Single Sign On between all applications.

I found this wonderful post about how to implement an oauth provider in devise by Chad Fowler & Albert Yi. This does not use omniauth, though its almost there. So, I decided to merge these two approaches and what do you know — it works!

Our current setup:

  • User Manager: devise + omniauth
  • App1: omniauth + custom gem for omniauth custom strategy
  • App2: omniauth + custom gem for omniauth custom strategy

I followed the User Manager setup must like the blog mentioned above for implementing an oauth provider. To start with, I created a new strategy for omniauth that would be invoked from App1 and App2. We added a custom gem for easy deployment — but the essence of the code is here:

require 'omniauth/oauth'
require 'multi_json'

module OmniAuth
 module Strategies
  class MyStrategy < OAuth2

   def initialize(app, api_key = nil, secret_key = nil, options = {}, &block)
     client_options = {
      :site =>  'http://myserver.local',
      :authorize_url => "http://myserver.local/auth/my_strategy/authorize",
      :access_token_url => "http://myserver.local/auth/my_strategy/access_token"
     }
     super(app, :my_strategy, api_key, secret_key, client_options, &block)
   end

protected

   def user_data
     @data ||= MultiJson.decode(@access_token.get("/auth/my_strategy/user.json"))
   end

   def request_phase
     options[:scope] ||= "read"
     super
   end

   def user_hash
     user_data
   end

   def auth_hash
     OmniAuth::Utils.deep_merge(super, {
       'uid' => user_data["uid"],
       'user_info' => user_data['user_info'],
       'extra' => {
         'admin' => user_data['extra']['admin'],
         'first_name' => user_data['extra']['first_name'],
         'last_name' => user_data['extra']['last_name'],
       }
     })
   end
  end
 end
end

Now, in my config/initializer/omniauth.rb I can add

Rails.application.config.middleware.use OmniAuth::Builder do
 provider :my_strategy, APP_ID, APP_SECRET
end

So, now we have the ‘OAuth Consumers’ or ‘OAuth Client’ (whichever you prefer to call it) for App1 and App2. The next step was to create the ‘Oauth Provider’ or ‘OAuth Server’. This we call the User Manager. You can follow instructions in the blog post mentioned above. I have skipped the details for the sake of brevity. In brief:

  • Create the standard devise User model and migration.
  • Create the Auth Controller actions (as show in the code snippet below)
  • Create the AccessGrant model (and if required the Authentication model)
  • Register the client applications (key and secret) via rails console on User Manager.
class AuthController < ApplicationController
 before_filter :authenticate_user!, :except => [:access_token]
 skip_before_filter :verify_authenticity_token, :only => [:access_token]

 def authorize
   # Find the Application using params[:client_id]
   # redirect to params[:redirect_uri]
 end

 def access_token
   # Check the params[:client_secret]
   # return Access Token
 end

 def user
   # Create the hash
   # return json hash
 end

Note: we are using a few of Devise before filters to authenticate the users before authorizing any application!

My User model had these Devise modules loaded:

class User < ActiveRecord::Base
 devise :database_authenticatable, :registerable,
        :token_authenticatable, :recoverable,
        :timeoutable, :trackable, :validatable

Now, the trick was to configure the User Manager with the standard omniauth providers. Here is the configuration:

Rails.application.config.middleware.use OmniAuth::Builder do
 provider :twitter, ApplicationConfig['TWITTER_APP_ID'], ApplicationConfig['TWITTER_APP_SECRET']
 provider :facebook, ApplicationConfig['FACEBOOK_APP_ID'], ApplicationConfig['FACEBOOK_APP_SECRET']
end

Now, when a user  logs in or signs up from App1:

  • the request is redirected to the UserManager via omniauth route ‘/auth/my_strategy’.
  • The User Manager signup can further redirect it to Twitter or Facebook using ‘/auth/twitter’ or ‘/auth/facebook’  and get the user to login / signup.
  • The request is redirected back to App1 via User Manager oauth provider callback uri.

This takes care of a single authentication system for the entire environment. Now we need to handle single sign on.

DO WE REALLY NEED TO? 🙂 This is where devise+omniauth combo rocked! I had the custom omniauth strategy configured on App2 also. Now, if a user had logged in App1 and got authorized. In the same session (depends on how you have devise configured — I had :timeoutable and :token_authenticatable) when the user accesses App2:

  • I added a standard before filter called ‘login_required’ which redirects to User Manager via ‘/auth/my_strategy’ if there is no current_user.
  • User Manager checks and finds a valid token and returns this to App2.
  • The user is automatically signed into App2.

Aim achieved! Seamless single-sign-on between multiple applications.

Update 1

I have extracted the code for this and have open-sourced it (finally)

The provider is at https://github.com/joshsoftware/sso-devise-omniauth-provider

The client is at https://github.com/joshsoftware/sso-devise-omniauth-client

I have updated the README for detailed instructions.

Update 2

Added account linking support. So if a user registers via Twitter and later tries to login via Facebook – he can link both these user accounts!

Update 3

Github code base is now updated to Rails 3.1.3. Thanks @robzolkos for helping out. The Rails30 branch is available for older the version.

Feel free to ask questions and give feedback in the comments section of this post. Thanks and Good Luck!