Expanding your ReSTful API's oAuth implementation to handle 3rd Party authentication

Adding an oAuth implementation into your ReSTful API is an essential method of authenticating requests between your client apps and your API.

Adding a ReSTful API for our iOS and Android apps allows us to centralize data to a cloud server, allowing us to share share amongst different users, clients and even platforms. By implementing oAuth, we have a standardized method to authenticate our ReST requests. If you are unfamiliar with the concepts of ReSTful APIs and oAUTH, I encourage you to read up on the concepts before continuing.

First, a quick and simple oAuth implementation:

Strategy:
– Create a Token Model, including a randomly generated access token, refresh token, an expiration date and a foreign key to the user.
– Create an onSave overload method that will hash our access and refresh token and save those fields into hashed_access_token columns instead of saving the actual tokens.
– Create a Controller with routes to authenticate a user (creating and returning a token), to logout a user (deleting a token), and to refresh a token.
– Add an interceptor on all requests to look for an authentication header. This interceptor will read the access token for the header, look for a valid access token provided and log in the user for that token by setting the current user to the session or request.

Now that we’ve gotten all that out of the way, let’s talk about external authorization.

External authorization means that some other authorization provider, like Facebook is generating an access token for us and we want to allow our API to accept that access token and link it to our user. There are a ton of different ways we could do this and the issue can get complicated quickly. After looking at a few implementations, I’ve come up with my method that I think is best.

Strategy:

1. Create an External Auth Prover table, which will serve to create a link between our user accounts and an account with another provider, like facebook. This will contain a foreign key to our user table, the name of the external provider and User’s id at that external provider (the user’s Facebook User ID for example).

2. Add a provider name column to our Token table, so that we can distinguish between access tokens that were generated by our system and those created from other providers. Note that we need to store the external tokens because if we don’t we will need to validate the external token every time we receive an authenticated request.
– in our Token model, add a method to override on Save which will check for the existence of an expiration date and if it doesn’t exist or is set for a period that is too long, set it to our default token expiration length. (this allows us to take in the provider, access token and expiration as parameters to our external auth endpoint).

3. Create a new external authorization endpoint that will allow the client to POST a user object and a token object. This endpoint should do the following:
– validate that the external token is valid and get the external user id for the token. This can be done in one step with facebook by calling graph.facebook.com/me?fields=id&access_token=[facebook_token].
– see if the user object that the client passed in contains an ID or an existing email. If so, they are attempting to create a link with an existing account. Validate that external account is not already linked with another user and if not then update the user with the user information passed in.
– If the user object does not create a user id or an existing email, it is a new user. So validate that the external account is not linked with any user in our system before continuing.
– Now save our user object, link the token we read in as a parameter with the user and save the token.
– Ensure that a record in our ExternalProvider table doesn’t already exist for this user / provider / external id combintation and if not, create one.
– The controller should return a json representation of the new token, just like the login method.

4. The authenication interceptor should now look for a provider header containing the provider name, and will now take into account the provider when we look at our Access Token table to log the user in.
– if the token is not found in our system, then the token may have been refreshed externally. To account for this, if the token / provider isn’t found in our system, then we should determine the external user id for the external token from the external provider, and look for an existing External Provider record in our system. If one is found, then create a new Token record for this new token assigned to that user and log that user in.

5. Now the client can allow the 3rd party libraries to handle all its access token stuff and can simply pass it in as an auth header, with a new provider header and our system can handle it seamlessly!

NOTE: The source code below in the example below is written in Ruby on Rails, but the concepts may be applied to any ReSTful API using oAuth.

#create our token and auth provider table #schema.rb create_table "tokens", force: true do |t| t.string "hashed_access_token", null: false t.string "hashed_refresh_token" t.date "expires_on" t.date "refresh_by" t.integer "user_id", null: false t.string "provider" end create_table "external_auth_providers", force: true do |t| t.string "provider_type", null: false t.string "provider_id", null: false t.integer "user_id", null: false end #add routes to log in, log out, refresh and external auth #config/routes.rb resources :tokens, :only => [:create,:destroy] do collection do put :refresh get :logout, to: 'tokens#destroy' post :external_auth end end #create our token model #models/Token.rb ... #before we save, hash our tokens before_validation :hash_tokens_and_check_expiration #hash our tokens def hash_tokens_and_check_expiration self.hashed_access_token = Digest::SHA2.hexdigest(self.access_token) if access_token self.hashed_refresh_token = Digest::SHA2.hexdigest(self.refresh_token) if refresh_token #ensure expires on is set, or within 7 days. If not, set to 7 days if !self.expires_on or self.expires_on > Time.zone.now + 7.days self.expires_on = Time.zone.now + 7.days end end #find a user for an external auth token stored on our server, or nil if #not valid def Token.user_for_stored_external_token(access_token, provider) hashed_access_token = Digest::SHA2.hexdigest(access_token) token = Token.find_by( :hashed_access_token => hashed_access_token, :provider => provider) if token and token.expires_on > Time.zone.now token.user else nil end end #add a load by token method and relationships #models/user.rb ... has_many :tokens has_many :external_auth_providers #load a user based on a token / provider combination def User.with_token token,provider if(provider) ExternalAuthProvider.user_for_token token,provider else Token.user_for_token token end end ... #add an external auth provider model #models/external_auth_provider.rb class ExternalAuthProvider < ActiveRecord::Base #external link to user belongs_to :user #call facebook graph to read the fb_id for a facebook token #if not valid, return nil def ExternalAuthProvider.fb_id_for fb_token #call graph api /me/fields=id, return id request_path = "https://graph.facebook.com/v2.5/me?fields=id&access_token=#{fb_token}" response = open(request_path).read json = JSON.parse(response) #read id from response json["id"] end #load the external id for any token / provider combination def ExternalAuthProvider.external_id_for_token(access_token, provider) if provider == PROVIDER_FACEBOOK ExternalAuthProvider.fb_id_for access_token end end #find the user for a token / provider combination def ExternalAuthProvider.user_for_token token,provider #first look for the given token and provider combo, where not expired. user = Token.user_for_stored_external_token token, provider #if not found then find the external id for the token, then load the record for that id and return the user. ## and then add the token if !user provider_id = ExternalAuthProvider.external_id_for_token token,provider provider = ExternalAuthProvider.find_by(:provider_id => provider_id, :provider_type => provider) if provider user = provider.user new_token = AccessToken.new new_token.access_token = token new_token.provider = provider new_token.user = user new_token.save! end end #return the user user end end #add a controller to handle all the oauth stuff #controllers/tokens_controller.rb ... #provide external authorization def external_auth #look at the external user id, provider and token temp_token = Token.new(params[:token]) provider = temp_token.provider ##Assumed Facebook temp_user = User.new(params[:user]) user = nil #validate the token first by pulling the external id for the token external_id = ExternalAuthProvider.external_id_for_token temp_token.access_token, temp_token.provider if !external_id render :json => { :error => "Invalid Token."} return end #now, see if this is an existing user, by loading from the user.id or the user email if temp_user.id #if there is a user id, then load that user. user = User.find(temp_user.id) #if there is no user id, find users by email elsif temp_user.email user = User.find_by(:email => temp_user.email) end #if we found a user, update its fields with the user parameters passed in if user user.update_attributes params[:user] #validate that this user isn't already linked with another account if !ExternalAuthProvider.where(:provider => provider).where(:provider_id => external_id).where.not(:user_id => user.id).empty? render :json => { :error => "Account is already linked."} return end #if there is no user then create one with the params specified and generate a password else #validate that this user isn't already linked with any account if !ExternalAuthProvider.where(:provider => provider).where(:provider_id => external_id).empty? render :json => { :error => "Account is already linked."} return end #generate a password since it is requred. temp_user.password = generated_password temp_user.password_confirmation = temp_user.password user = temp_user end #validate the everything saves correctly and link the new token with the user if user.save and temp_token.user = user and temp_token.save #load an auth record for the provider, user and id if it exists and if not then create our new link e = ExternalAuthProvider.find_by :provider_type => provider, :user => user, :provider_id => external_id if(!e) e = ExternalAuthProvider.new e.provider_id = external_id e.provider_type = provider e.user = user e.save! end #return the token and user. render :json => temp_token, :include => :user, :methods => [:access_token, :refresh_token], :except => [:hashed_access_token, :hashed_refresh_token] else render :json => { :error => "Error creating external user or token"} end end end #add an application level interceptor #controllers/application.rb before_filter :login_with_access_token #the interceptor should examine the token and log the user in if valid #helpers/application.rb def login_with_access_token if request.headers['Authorization'] token = request.headers['Authorization'].split('Bearer ').last elsif params[:access_token] token = params[:access_token] end if token user = User.with_token token, request.headers['Provider'] if user @current_user = user end end end

To View or download Full source code, integrated into my Rails Starter API, view it on github, here: https://github.com/logansease/base_rails_api

Comments

  Comments

This site uses Akismet to reduce spam. Learn how your comment data is processed.

-->