Expanding your ReSTful API's oAuth implementation to handle 3rd Party authentication
Warning: count(): Parameter must be an array or an object that implements Countable in /home/customer/www/logansease.com/public_html/iparty/wp-content/themes/grizzly-theme/base/shortcodes/fix.php on line 36
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