TechHui

Hawaiʻi's Technology Community

Google API Authorization Using Oauth2 in Ruby on Rails

Say you are writing a web app that will help its users optimize their companies' online marketing.  In that case your web app would need to be able to access its users google analytics web site data.  The way you allow your site users to authorize your web app to to access their google analytics data, is with Oauth2. Oauth2 is a security protocol for third party application authorization.  When a user visits your web app, you present them with a link that will take them to a google page where they will be able to authorize your app to access their data.  Google knows who your user is because your user is logged into Google and if not, Google will ask them to log in.  The way Google knows what the web app the user is giving permission to access their google analytics data, is via parameters in the url the user clicked on your site.  Those parameters tell Google to what Google project, your site user is giving authorization to access their data.  So, for your web app, you will need one Google project associated with it, and each of your users will need to have a Google app that they are authorized to access, and whose data they will authorize your Google Project to access.  Your site users will in effect be giving the Google Project you set up for your site, access to their Google Analytics data.  An example of a Google app might be: read-only access to Google Analytics.  So, if user A has been authorized to access the Google Analytics data for site X, then user A can log into your site, and then authorize your site to access Google Analytics data for site X on behalf of user A.  Your site will then be able to access Google Analytics data for site X on behalf of user A until user A revokes access to your site.  Once a site user who has been directed to Google via an authorize link on your site, clicks a button on the Google site indicating that they want your site to be able to access their data, they will be redirected to your site, and the redirect url will have a code.  Your site will use that code to send a server to server message to google, which will retrurn an access token and a refresh tokens.  Google access tokens last one hour, but you can get new ones via the refresh token. A lot of this is taken care of for you by the Oauth2 Ruby library (gem).

For this blog post, I am assuming you are using devise, and therefore can invoke current_user in your controllers to get the currently logged in user.  I am also assuming that you add a field to your users table called ggl_access_token to store a serialized version of a hash containing the access and refresh tokens, and a field, and another field called google_profile_id to store the application id of the site for which users want your web app to access Google Analytics API data.

For your Rails app, you will need the Oauth2 and Legato gems.  You can add these lines to your Gemfile:

gem 'oauth2',
gem 'legato'

The Oauth2 gem is for authorization, and the legato gem is for querying Google data in a Ruby idiomatic way, without the json commands that Google expects, and you'll use it to allow a site user to choose what app he wants your site to access data for. 

For your site, you will need a Google Project.  To create a google project, which is technically what your site users are giving permission to, to access their data.  You can view your current google projects and add new ones here:

https://console.developers.google.com/

Create a project.  Then click on "Credentials" under the "APIs & Auth" menu header to your left.  You will then see the Client ID and Client Secret for each of your projects.  Refer to the ones for the project that you are using to access the Google APIs for this web app.  You can click on "Edit Settings" within a project, to add the url you will be using for the authorization callback.  For example:

http://localhost:3000/authorize_google_api

This will need to match up with what you assign to the GOOGLE_REDIRECT_HOST environment variable.

the following should be in a Rails environment variables file:

ENV['GOOGLE_OAUTH_CLIENT_ID'] = ''"
ENV['GOOGLE_OAUTH_SECRET_KEY'] = ''"
ENV['GOOGLE_REDIRECT_HOST'] = 'http://localhost:3000'

ENV['GOOGLE_REDIRECT_PATH'] = '/authorize_google_api'

I load mine at application startup via code in config/environment.rb

env_vars = File.join(Rails.root, 'config', 'env_vars.rb')
load(env_vars) if File.exists?(env_vars)

of course, the first two environment variables should not be empty strings but the values you got from the Google developer's console page under the Credentials section, and the last two will change depending on whether you are in development or production, what your application's web address is, and the value you choose for the redirect path for your app.  Just make sure to add the resulting url to the authorized callbacks in the Google developer's console page, or the callback and authorization will fail.

Add a link on your site that asks your users to authorize your site to access their Google data like so:

Click  <%= link_to 'here',  @google_oauth_service.get_authorize_url %>  to authorize us to access your app.

This takes advantage of a GoogleOauthService object (in @google_oauth_service), that is set up in a before_action filter, that we will set up below.

Put the following onto a controller of your choice, possibly a new one, and add a route to it:

i.e. in a controller called oauth_authorize:

def authorize_google_api
  google_oauth_service = GoogleOauthService.new(current_user)
  google_oauth_service.set_access_token(params[:code])
  google_oauth_service.refresh_access_token
  redirect_to root_path, :notice => "you have been successfully authorized!"
end

in routes.rb:

get 'authorize_google_api' => 'oauth_authorize#authorize_google_api'

This will handle the Google callback, once the user clicks on the authorize button at the Google site, and is redirected back to your app.

put the following into your application controller or, better yet if you are using Rails 4, into a controller concern and include that concern into the site's application controller:

def refresh_token

  @google_oauth_service.refresh_access_token
end

def google_oauth_service
  @google_oauth_service = GoogleOauthService.new(current_user, current_user.google_profile_id)
  if current_user.ggl_acc_token
    @google_oauth_service.restore_access_token
    if @google_oauth_service.expired?
      refresh_token
    end
  end
end

Then use a before_action in any controller or for any controller where you need to access the Google API on behalf of a site user:

before_action :google_oauth_service

put the following into a file inside app/services folder:

require 'oauth2'

class GoogleOauthService

  GOOGLE_OAUTH_CLIENT_ID = ENV['GOOGLE_OAUTH_CLIENT_ID']
  GOOGLE_OAUTH_SECRET_KEY = ENV['GOOGLE_OAUTH_SECRET_KEY']
  GOOGLE_REDIRECT_HOST = ENV['GOOGLE_REDIRECT_HOST']

  GOOGLE_REDIRECT_PATH = ENV['GOOGLE_REDIRECT_PATH']
  GOOGLE_REDIRECT_URI = "#{GOOGLE_REDIRECT_HOST}#{ENV['GOOGLE_REDIRECT_PATH']}"
  QUERY_ROOT = "https://www.googleapis.com/analytics/v3/data/ga?"

  attr_reader :ga_id, :oauth_auth_code, :client, :user

  def initialize(user, ga_id=nil)
    @user = user
    @ga_id = ga_id if ga_id
    @client = OAuth2::Client.new(GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_SECRET_KEY, {
      :authorize_url => 'https://accounts.google.com/o/oauth2/auth',
      :token_url => 'https://accounts.google.com/o/oauth2/token'
    })
  end

  def get_authorize_url
    url = @client.auth_code.authorize_url({
      :scope => 'https://www.googleapis.com/auth/analytics.readonly',
      :redirect_uri => GOOGLE_REDIRECT_URI,
      :access_type => 'offline'
    })
    url #copy this url into browser
  end

  def set_access_token(code=nil)
    @access_token = @client.auth_code.get_token(code || @oauth_auth_code,
      {:redirect_uri => GOOGLE_REDIRECT_URI})
    @serialized_access_token = @access_token.to_hash.to_json
    user.update_attribute(:ggl_acc_token, @serialized_access_token)
    nil
  end

  def get_sessions_and_pageviews_by_country
    query = "start-date=2015-01-01&end-date=2015-02-01&metrics=ga:sessions,ga:pageviews&dimensions=ga:country"
    response_json = @access_token.get("#{QUERY_ROOT}ids=ga:#{@ga_id}&#{query}").body
    JSON.parse(response_json)
  end

  def execute_query(query)
    response_json = @access_token.get("#{QUERY_ROOT}ids=ga:#{@ga_id}&#{query}").body
    JSON.parse(response_json)
  end

  def restore_access_token
    @serialized_access_token = user.ggl_acc_token
    @access_token = OAuth2::AccessToken.from_hash @client,
    {:refresh_token => JSON.parse(@serialized_access_token)['refresh_token'],
    :access_token => JSON.parse(@serialized_access_token)['access_token'],
    :expires_at => JSON.parse(@serialized_access_token)['expires_at']}
    nil
  end

  def expired?
    restore_access_token unless @access_token
    @access_token.expired?
  end

  def access_token_object
    restore_access_token unless @access_token
    @access_token
  end

  def access_token
    restore_access_token unless @access_token
    @access_token.token
  end

  def refresh_token
    restore_access_token unless @access_token
    @access_token.refresh_token
  end

  def refresh_access_token
    restore_access_token unless @access_token
    @access_token = @access_token.refresh!
    @serialized_access_token = @access_token.to_hash.to_json
    user.update_attribute(:ggl_acc_token, @serialized_access_token)
    nil
  end

end

This service class is a wrapper around the OAuth2 Ruby library.  It serves mainly to encapsulate the needed OAuth2 functionality and isolate the controller or concern from OAuth2 library details.  The thing to keep in mind is that the OAuth2::AccessToken object (http://www.rubydoc.info/github/intridea/oauth2/OAuth2/AccessToken), has both a refresh and access tokens inside it, so it's deceptively named.  When the access token inside the OAuth2::AccessToken object has expired, which you can tell by calling expired? on it, you just call the refresh! method, and the oauth library will refresh the access token and possibly even refresh the refresh token, depending on how old the refresh token is.  Also, notice that OAuth2::AccessToken has a to_hash method for serializing it so you can store it in a database field, and a from_hash method for restoring it from its serialized form.  Also, as long as the client id client secret, callback domain, and callback path do not change for the Google Project you are using for your app, you use for your cache the authorization link returned by get_authorize_url, since it depends only on your site and the Google Project associated with your site, and not on the details of a specific site user or their Google apps.

You will need to allow site users to pick which of their Google Apps they want your site to access data for.  You can do this with a select box.  Here is some controller code to get the list for a given user:

def get_google_profiles
  google_user = Legato::User.new(@google_oauth_service.access_token_object)
  @profiles = google_user.profiles.map{|profile| GoogleProfile.new(profile.id, profile.name)}
end

Store whatever app id a site user chooses in the google_profile_id in that user's row in the users table.

For testing, you can play the part of a user, and revoke authorization at the following url:

https://security.google.com/settings/security/permissions?pli=1

Of course, to do that, you will need to be authorized as a user on some Google application, possibly Google Analytics for some web site.

Of course, a site user might revoke access to their Google app at some point, and in that case, your site would have no way of knowing until they actually try to refresh an expired access token or make a query using an existing non-expired access token.  So you want to be able to catch such errors.  To do that, you can add a rescue_from call to your application controller, or a concern that you include into the controller:

rescue_from OAuth2::Error do |oauth2_error|
  if current_user.ggl_acc_token
  begin
      refresh_token
      redirect_to root_path, :notice => "Your google token has been refreshed."
    rescue Exception => exception
      oauth_error
    end
  else
    oauth_error
  end
end

def oauth_error
  if current_user.ggl_acc_token
    current_user.update_attribute(:ggl_acc_token, nil)
  end
  redirect_to root_path, :notice => "You need to reauthorize our site to use access your data."
end

I borrowed heavily from the following page in writing this code and this blog post:

https://github.com/tpitale/legato/wiki/OAuth2-and-Google

Views: 3195

Comments are closed for this blog post

Sponsors

web design, web development, localization

© 2024   Created by Daniel Leuck.   Powered by

Badges  |  Report an Issue  |  Terms of Service