Question

I am hoping someone with more experience can help me get my head round this Google API.

I am just building a demo app based off the ruby quickstart sample app, to explore this API. I have a Rails 4.0 app and have successfully (for the most part) installed the Google+ sign in.

It all goes wrong though once the access token for the user expires.

What my test app does successfully:

  1. Signs the user in and retrieves access token for client side along with server code.
  2. Exchanges the server code for access token & refresh token & id token
  3. Creates token pair object that holds access token and refresh token, then stores it in a session hash
  4. My app can then make requests to get user people list, insert moments etc.

So my question is, what is the correct way to get a new access token with the refresh token?

With the code below, after the access token expires I get error of "Invalid Credentials"

If I call $client.authorization.refresh! then I get error of "Invalid Request"

config/initializers/gplus.rb

# Build the global client
$credentials = Google::APIClient::ClientSecrets.load
$authorization = Signet::OAuth2::Client.new(
    :authorization_uri => $credentials.authorization_uri,
    :token_credential_uri => $credentials.token_credential_uri,
    :client_id => $credentials.client_id,
    :client_secret => $credentials.client_secret,
    :redirect_uri => $credentials.redirect_uris.first,
    :scope => 'https://www.googleapis.com/auth/plus.login',
    :request_visible_actions => 'http://schemas.google.com/AddActivity',
    :accesstype => 'offline')
$client = Google::APIClient.new(application_name: " App", application_version: "0.1")

*app/controllers/google_plus_controller.rb*

class GooglePlusController < ApplicationController

  respond_to :json, :js

  def callback

    if !session[:token]
      # Make sure that the state we set on the client matches the state sent
      # in the request to protect against request forgery.
      logger.info("user has no token")
      if session[:_csrf_token] == params[:state]
        # Upgrade the code into a token object.
        $authorization.code = request.body.read
        # exchange the one time code for an access_token, id_token and refresh_token from Google API server
        $authorization.fetch_access_token!
        # update the global server client with the new tokens
        $client.authorization = $authorization

        # Verify the issued token matches the user and client.
        oauth2 = $client.discovered_api('oauth2','v2')
        tokeninfo = JSON.parse($client.execute(oauth2.tokeninfo, 
          :access_token => $client.authorization.access_token, 
          :id_token => $client.authorization.id_token).response.body)

        # skipped

        token_pair = TokenPair.new
        token_pair.update_token!($client.authorization)
        session[:token] = token_pair
      else
        respond_with do |format|
          format.json { render json: {errors: ['The client state does not match the server state.']}, status: 401}
        end
      end # if session csrf token matches params token
      # render nothing: true, status: 200
    else
      logger.info("user HAS token")

    end # if no session token
    render nothing: true, status: 200
  end #connect


  def people
    # Check for stored credentials in the current user's session.
    if !session[:token]
      respond_with do |format|
        format.json { render json: {errors: ["User is not connected"]}, status: 401}
      end
    end
    # Authorize the client and construct a Google+ service 
    $client.authorization.update_token!(session[:token].to_hash)

    plus = $client.discovered_api('plus', 'v1')
    # Get the list of people as JSON and return it.
    response = $client.execute!(api_method: plus.people.list, parameters: {
        :collection => 'visible',
        :userId => 'me'}).body
    render json: response
  end

  #skipped
end

Any help appreciated. Extra questions, the sample app I'm using as a guide builds a global authorization object (Signet::OAuth2::Client.new) - however other documentation I have read over the last day has stated building an authorization object for each API request. Which is correct?

Was it helpful?

Solution 2

The line saying: accesstype => 'offline'

should say: access_type => 'offline'

OTHER TIPS

This is a fragment I use in an app:

require 'google/api_client'

        if client.authorization.expired? && client.authorization.refresh_token
            #Authorization Has Expired
            begin
                client.authorization.grant_type = 'refresh_token'
                token_hash = client.authorization.fetch_access_token!   
                goog_auth.access_token = token_hash['access_token']
                client.authorization.expires_in = goog_auth.expires_in || 3600
                client.authorization.issued_at = goog_auth.issued_at = Time.now
                goog_auth.save!
            rescue
                redirect_to user_omniauth_authorize_path(:google_oauth2)
            end

I am using omniauth OAuth2 (https://github.com/intridea/omniauth-oauth2), omniauth-google-oauth2 (https://github.com/zquestz/omniauth-google-oauth2), and google-api-ruby-client (https://code.google.com/p/google-api-ruby-client/).

Walk through:

1) If the access token is expired & I have a refresh token saved in the DB, then I try a access token refresh.

2) I set the grant type to "refresh_token" & then the "fetch_access_token!" call returns a plain old hash.

3) I use the 'access_token' key to return the new valid access token. The rest of the key/values can pretty much be ignored.

4) I set the "expires_in" attr to 3600 (1hr), and the "issued_at" to Time.now & save.

NOTE: Make sure to set expires_in first & issued_at second. The underlying Signet OAuth2 client resets your OAuth2 client issued_at value to Time.now, not the value you set from the DB, and you will find that all calls to "expired?" return false. I will post Signet source at the bottom.

5) If the access code is expired and I do NOT have a refresh token, I redirect to the omniauth path, and start the whole process over from scratch, which you have already outlined.

Note: I save almost everything in the DB: access_token, refresh_token, even the auth_code. I use the figaro gem to save env specific values such as client_id, client_secret, oauth2_redirect, and things like that. If you use multiple env's to develop, use figaro (https://github.com/laserlemon/figaro).

Here is the Signet source that shows how setting expires_in manually actually resets issued_at to Time.now(!). So you need to set "expires_in" first & THEN "issued_at" using the issued_at value you have from the DB. Setting expires_in second will actually RESET your issued_at time to Time.now... eg; calls to "expired?" will ALWAYS return false!

How do I know this? As Mr T would say, "Pain."

http://signet.rubyforge.org/api/Signet/OAuth2/Client.html#issued_at%3D-instance_method

# File 'lib/signet/oauth_2/client.rb', line 555
def expires_in=(new_expires_in)
if new_expires_in != nil
@expires_in = new_expires_in.to_i
@issued_at = Time.now
else
@expires_in, @issued_at = nil, nil

end

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top