The line saying: accesstype => 'offline'
should say: access_type => 'offline'
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:
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?
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