HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyrailsMinor

Rails service + OAuth

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
railsoauthservice

Problem

I'm having trouble structuring the logic of OAuth integration into my web app. In the app, a user creates a Report that consists of data from their Google Analytics account.

User steps:

  • User clicks 'New Report'



  • Google presents the 'Allow Access' page for OAuth access



  • User is presented with a list of their GA web properties and selects one



  • A report is created using data from the selected web property



My issue is in structuring the code below.

When the user clicks "New Report", they are actually redirected to google_analytics#ga_session to begin the authorization process. The code to retrieve the user's web properties succeeds, but the code at the bottom needs to be refactored so it is reusable when retrieving web property data. The main two issues I can't figure out is how to make the Google Analytics instance reusable and how to structure the OAuth redirects.

Retrieve web properties:

GoogleAnalyticsController

def ga_session
    client = OAuth2::Client.new(ENV['GA_CLIENT_ID'], ENV['GA_SECRET_KEY'], {
        :authorize_url => 'https://accounts.google.com/o/oauth2/auth',
        :token_url => 'https://accounts.google.com/o/oauth2/token'
    })
    redirect_to client.auth_code.authorize_url({
         :scope => 'https://www.googleapis.com/auth/analytics.readonly',
         :redirect_uri => ENV['GA_OAUTH_REDIRECT_URL'],
         :access_type => 'offline'
     })
  end

  def oauth_callback
    session[:oauth_code] = params[:code]
    redirect_to new_report_path
  end


ReportsController

def new
     @report = Report.new
     ga_obj = GoogleAnalytics.new
     ga_obj.initialize_ga_session(session[:oauth_code])
     @ga_web_properties = ga_obj.fetch_web_properties
end


GoogleAnalytics model

```
def initialize_ga_session(oauth_code)
client = OAuth2::Client.new(ENV['GA_CLIENT_ID'], ENV['GA_SECRET_KEY'], {
:authorize_url => 'https://accounts.google.com/o/oauth2/auth',
:token_url => 'https://accounts.google.com/o/oau

Solution

The first step would be to refactor out the instanciation of the Oauth2 client:

# lib/analytics_oauth2_client
class AnalyticsOAuth2Client > OAuth2::Client
  def initialize(client_id, client_secret, options = {}, &block)
    client_id || = ENV['GA_CLIENT_ID']
    client_secret || = ENV['GA_SECRET_KEY']
    options.merge!(
        authorize_url: 'https://accounts.google.com/o/oauth2/auth',
          token_url: 'https://accounts.google.com/o/oauth2/token'
    )   
    super(client_id, client_secret, options, &block)
  end 
end


And then maybe we should devise a strategy to save those pesky oauth tokens:

class AccessToken  ENV['GA_OAUTH_REDIRECT_URL'])
    @saved_token = AccessToken.create!(token.to_hash)
    session[:oauth_token] = @saved_token.id
    redirect_to new_report_path
  end
end


And we need a controller method to get the access token from the session:

def authorize!
  # ... @todo redirect to back to authentication if no `session[:oauth_token]`
  stored_token = Token.find!(session[:oauth_token])
  @token = OAuth2::AccessToken(AnalyticsOAuth2Client.new, stored_token.attributes)
  @client = GoogleAnalytics.new(token: @token)
end


And we need to shove the token into GoogleAnalytics:

class GoogleAnalytics
  include ActiveModel::Model

  attr_writer :token
  attr_accessor :user

  def initialize(attrs: {})
    super
    @user ||= Legato::User.new(@token) if @token
  end
end


Then we consider the separation of concerns - models should not deal with authentication - thats a controller concern. You should pass whatever authorized object the models needs
from the controller. Dependency injection is commonly used for this:

class Report
  attr_writer :client

  def get_keywords(oauth_code)
    self.url = @client.fetch_url(self.web_property_id)
    self.keywords = # Get keywords for self.url from another service
    keyword_revenue_data(oauth_code)
  end
end

class ReportsController
  # ...

  def create
    @report = Report.new(client: @client, params[:report])
    @report.get_top_traffic_keywords
    create!
  end

  # ...
end

Code Snippets

# lib/analytics_oauth2_client
class AnalyticsOAuth2Client > OAuth2::Client
  def initialize(client_id, client_secret, options = {}, &block)
    client_id || = ENV['GA_CLIENT_ID']
    client_secret || = ENV['GA_SECRET_KEY']
    options.merge!(
        authorize_url: 'https://accounts.google.com/o/oauth2/auth',
          token_url: 'https://accounts.google.com/o/oauth2/token'
    )   
    super(client_id, client_secret, options, &block)
  end 
end
class AccessToken < ActiveRecord::Base
  # @expires_at [Int]
  # @access_token [String]
  # @refresh_token [String]
end

class GoogleAnalyticsController

  # ...
  def oauth_callback
    @client = AnalyticsOAuth2Client.new
    token = @client.auth_code.get_token(params[:code], :redirect_uri => ENV['GA_OAUTH_REDIRECT_URL'])
    @saved_token = AccessToken.create!(token.to_hash)
    session[:oauth_token] = @saved_token.id
    redirect_to new_report_path
  end
end
def authorize!
  # ... @todo redirect to back to authentication if no `session[:oauth_token]`
  stored_token = Token.find!(session[:oauth_token])
  @token = OAuth2::AccessToken(AnalyticsOAuth2Client.new, stored_token.attributes)
  @client = GoogleAnalytics.new(token: @token)
end
class GoogleAnalytics
  include ActiveModel::Model

  attr_writer :token
  attr_accessor :user

  def initialize(attrs: {})
    super
    @user ||= Legato::User.new(@token) if @token
  end
end
class Report
  attr_writer :client

  def get_keywords(oauth_code)
    self.url = @client.fetch_url(self.web_property_id)
    self.keywords = # Get keywords for self.url from another service
    keyword_revenue_data(oauth_code)
  end
end

class ReportsController
  # ...

  def create
    @report = Report.new(client: @client, params[:report])
    @report.get_top_traffic_keywords
    create!
  end

  # ...
end

Context

StackExchange Code Review Q#27144, answer score: 2

Revisions (0)

No revisions yet.