Skip to content
Jan Stastny edited this page Aug 30, 2017 · 4 revisions

Authenticating resource through 3rd party application

The general scenario for using this gem assumes you are running a web app on the server that tries to authenticate a resource against a 3rd party Oauth2 provider. Depending on the Oauth2 provider, there may be some limitations when using a standard authentication endpoint (eg. Google limits the possibility of authenticating from non-standard locations /aka. IPs/, which is probable as your app server is located elsewhere then the client). Therefore, appropriate workflow should be implemented - examples for the most popular providers can be found below.

General approach

  1. Client app (browser client, mobile app) obtains a short living auth code for the required scope.
  2. The auth_code and provider name is send to our application:
$.post 'https://domain.com/oauth2/token',
  client_id: d017e2ecd288d3141478af6667d9b7e1401e40b9c8e6290c52ad345dbd7b99fe
  client_secret: 67ac711924cc8c46cd4395c13cfe4af91eda7408dc27afbf030d3492445b02ffc
  grant_type: assertion
  provider: <provider_name>
  assertion: <auth_code>
  1. Server calls 3rd party OAuth2 endpoint and exchanges auth_code for access_token, that can be used to obtain user data, allowed by the scope.

We will assume the provider-specific code is defined in a separate classes (eg. ExternalAuth::Google), which examples you will find below.

Google

The strategy described in the introduction is fully supported by Google as CrossClientAuth. Please follow the steps in the documentation provided by google (setting up an application in the Google API console, create server API key). The application should request auth code (one time authorization code) for user from Google API (which can be later exchanged for access token by the server) requiring the following permissions:

  • profile
  • email

The following request can be then constructed:

$.post 'https://domain.com/oauth2/token',
  client_id: d017e2ecd288d3141478af6667d9b7e1401e40b9c8e6dd290c52ad345bd7b99fe8
  client_secret: 67ac711924cc8c46cd4395c13cfe4af91eda740dd8dc27afbf0303492445b02ffc
  grant_type: assertion
  provider: google
  assertion: <auth_token>

Then we need to add the following declaration to config/initializers/doorkeeper.rb:

resource_owner_from_assertion do
    provider = params[:provider]
    if provider == "google"
      g = ExternalAuthorize::Google.new(params[:assertion])
      g.get_user!
    end
end

And the class itself might look like:

# extenal_auth_google.rb
module ExternalAuthorize
  class Google
      def initialize(access_code)
        @access_code = access_code
        @user_data = user_data
      end

      def app_auth_data
        return {client_id: Rails.application.secrets[:google_api_key], client_secret: Rails.application.secrets[:google_api_secret]}
      end

      def fetch_access_token
        if @access_token.present?
          @access_token
        else
          uri = URI.parse("https://www.googleapis.com/oauth2/v3/token")
          params = {
              client_id: app_auth_data[:client_id],
              client_secret: app_auth_data[:client_secret],
              grant_type: 'authorization_code',
              code: @access_code
          }
          response = Net::HTTP.post_form(uri, params)
          data = JSON.parse(response.body)
          if data['access_token'].present?
            @access_token ||= data['access_token']
          else
            puts data
            nil
          end
        end
      end

    def user_data
      access_token = fetch_access_token
      if access_token.present?
        google = URI.parse('https://www.googleapis.com/plus/v1/people/me?access_token=' +
                               access_token)
        response = Net::HTTP.get_response(google)
        JSON.parse(response.body)
      else
        nil
      end
    end

    def get_user!
      if @user_data.present?
        # below you should implement the logic to find/create a user in your app basing on @user_data
        # It should return a user object
        User.authorize_from_external(@user_data)
      else
        nil
      end
    end
  end
end

Facebook

Unlike google, facebook does not offer any special scenario for server app authorization on behalf of the user.

The application should authenticate user against Facebook Graph API requiring the following permissions:

As a result, app should get valid OAuth2 access token, that should be used to authenticate user against this API. The parameters obtained from the given access token can be checked by querying: GET https://graph.facebook.com/me?access_token=<access_token>. Please ensure that the email is returned in the response.

The following request can be then constructed:

$.post 'https://domain.com/oauth2/token',
  client_id: d017e2ecd288d3141478af6667d9b7e1401e40b9c8e6dd290c52ad345bd7b99fe8
  client_secret: 67ac711924cc8c46cd4395c13cfe4af91eda740dd8dc27afbf0303492445b02ffc
  grant_type: assertion
  provider: facebook
  assertion: <access_token>

Then we need to add the following declaration to config/initializers/doorkeeper.rb:

resource_owner_from_assertion do
    provider = params[:provider]
    if provider == "facebook"
      g = ExternalAuthorize::Facebook.new(params[:assertion])
      g.get_user!
    end
end

And the class itself might look like:

# require 'google/api_client'
module ExternalAuthorize
  class Facebook
    def initialize(auth_code)
      @auth_code = auth_code
      @user_data = user_data
    end

    def user_data
      facebook = URI.parse("https://graph.facebook.com/v2.5/me?access_token=#{@auth_code}&fields=id,name,email,first_name,last_name,gender")
      response = Net::HTTP.get_response(facebook)
      JSON.parse(response.body)
    end

    def user_data_req
      "https://graph.facebook.com/v2.5/me?access_token=#{@auth_code}&fields=id,name,email,first_name,last_name,gender"
    end

    def image
      img = URI.parse("https://graph.facebook.com/v2.5/me/picture?access_token=#{@auth_code}&width=180&height=180&redirect=false")
      response = Net::HTTP.get_response(img)
      JSON.parse(response.body)
    end

    def get_user!
      if user_data.present?
        # below you should implement the logic to find/create a user in your app basing on @user_data
        # It should return a user object
        User.authorize_from_external(@user_data)
      else
        nil
      end

    end
  end
end