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.
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.
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.
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.
Leave a Reply