A Sane Oauth Federation Strategy With Doorkeeper in Ruby

There are a lot of articles out there about setting up one server with Doorkeeper to offer Oauth support in Ruby projects. But when you start to get into federating your Oauth credentials across services it turns into the wild west. I saw a few examples where people would set up doorkeeper on every service. That may work for some but for us it didn’t feel like the right solution.
Oauth - New Page (1)
In our case we wanted to allow the user to login once and use any of our services with Oauth providing the security for all of our services. Setting up doorkeeper for each service would get crazy after a while. Now the client has to maintain a separate set of tokens for every service and if you want to sync them up to federate a logout you have extra layers of complexity.

Delegation

One way to solve this could be to use delegation. In this scenario all of the services can take oauth requests, but then they pass these requests on to one service that is responsible for all Oauth functionality as the system of record. This is in addition to regular requests that include the authentication token which would require a call out to the main Oauth service.
Oauth passthrough - New Page
There are a few problems with this. First, the main Oauth service is going to get a lot of traffic. Second, that Oauth service is a major point of failure and third how do you handle race conditions when a token expires and a refresh token is requested?

A Hybrid Approach

We ended up adopting a hybrid approach to get the best of both worlds.
Oauth cached - New Page
All of our Oauth functionality is centralized in one service. Now all calls for grant tokens, and token requests from a grant or refresh token go against our one Oauth service. The other services contain a data store (you could use Redis or a SQL database for this). When a request comes in with a Oauth authentication token, if it’s the first time this token has been used with this service, a call is made to the main Oauth service using a secure connection to a custom endpoint. The endpoint verifies if the token is valid. If it is the service returns any user specific metadata we need along with the expiration DateTime. These values are written in the data store on calling service and then the request is serviced successfully. On subsequent requests with the same token the values from the data store are used and no calls go back to the main Oauth service. When the token expires the service returns a 401 and the client is responsible for using the refresh token (if you are using one) to get another authentication token from the Oauth Doorkeeper based service etc.

Show Me the Code

In our services we add a method in our ApplicationController that looked something like the following

class ApplicationController < ActionController::API
  def check_authorization
    authorization = request.headers['Authorization']
    if authorization
      @user = User.where(token: authorization).last
      if !@user or @user.expires_at < DateTime.now
        party_response = HTTParty.get("http://our_service_url/check_key.json", query: {'signature' => our_unique_req_signature, 'oauth_token' => authorization})        
        parsed_response = party_response.parsed_response
        if parsed_response['user_id']
          @user = User.where(user_id: parsed_response['user_id']).last
          @user = @user? @user : User.new
          @user.update_attributes(user_id: parsed_response['user_id'],
                                email: parsed_response['email'],
                                token: authorization,
                                expires_at: Marshal.load(parsed_response['expires_at'].force_encoding('UTF-8')))
        else
          response.status = 401
          render json: {authorized: false} and return
        end
      end
    else
      response.status = 401
      render json: {authorized: false} and return
    end
  end
end

This code is then used as a before_filter for any actions that need to be protected.

In our Oauth service we add an action to verify the signature.

  def check_key
    if signature_valid? params['signature']
      access_token = Doorkeeper::AccessToken.where(token: params['oauth_token']).last
      provider = nil
      if access_token and access_token.resource_owner_id
        provider = Provider.find access_token.resource_owner_id
      end

      if provider and provider.user_metadata
        metadata = JSON.parse provider.user_metadata
        expires_at = access_token.created_at + access_token.expires_in
        metadata['expires_at'] = Marshal.dump(expires_at.to_s.force_encoding("ISO-8859-1"))  
        render json: metadata
      else
        response.status = 401
        render json: {}
      end
    else
      response.status = 401
      render json: {}
    end    
  end

The Provider object is whatever you set up your resource_owner_id to point to in Doorkeeper. The Doorkeeper::AccessToken is where Doorkeeper stores all of its access tokens.

Now we have one system of record for all Oauth authentication, things are federated, we have maintained a secure system and can handle a large volume of requests throughout the system. The only thing that the client needs to worry about is one Oauth Token instead of three.

About Me: I am a Atlanta based, mobile/Android/IOS/AngularJS/Ruby developer, polyglot programmer, founder of Polyglot Programming Inc., wearable technology enthusiast and am interested in the internet of things. You will often find me purr programming and I regularly speak at conferences around the world. I am available for hire! More Posts

Follow Me:
TwitterLinkedInGoogle Plus

I am a Atlanta based, mobile/Android/IOS/AngularJS/Ruby developer, polyglot programmer, founder of Polyglot Programming Inc., wearable technology enthusiast and am interested in the internet of things. You will often find me purr programming and I regularly speak at conferences around the world. I am available for hire!

Posted in Architecture, Development, gems, rails, ruby, scaling Tagged with: , , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *

*