I'm having some problem setting up an oauth provider with two-legged authentication.
I'm using the oauth-plugin gem and the oauth gem, everything works fine except for my "update" requests. The signature verification process keeps failing.
Here is what I'm doing:
In the client, I'm using
oauth = OAuth::AccessToken.new(OAuth::Consumer.new(app_key, app_secret, :site => #api_endpoint))
oauth.get("http://localhost/api/v1/users/1")
oauth.post("http://localhost/api/v1/users", {:email => "testemail#mysite.com"})
oauth.put("http://localhost/api/v1/users", {:tags => ["some", "new", "tags"]})
oauth.delete("http://localhost/api/v1/users/1")
get, post and delete all go through authentication fine, but update fails.
On the server side, I have my ClientApplication class set up
def self.verify_request(request, options = {}, &block)
begin
signature = OAuth::Signature.build(request, options, &block)
return false unless OauthNonce.remember(signature.request.nonce, signature.request.timestamp)
value = signature.verify
value
rescue OAuth::Signature::UnknownSignatureMethod => e
false
end
end
signature.verify fails on my update requests and passes on the other 3 requests. Anybody know what's happening?
Turns out the problem is with passing the params through the body.
I moved the params into the url with Addressable/uri, and that fixed the problem.
It's gonna be a little limiting in terms of length, but ok for now.
Related
I am stuck / block on this point, I keep on having this error message 'Code was already redeemed'
3 steps :
Getting the authorization URL (SignetLogin)
Calling it and retrieving the authorization code (SignetAuth)
Getting the refresh token and calling google API (SignetInsert)
When I try to use the API I always get this error message : Code was already redeemed'.
I ask for some help, what's wrong and where can I find some tips about it ? What did I missed ?
Thanks - Gregoire
def SignetLogin
auth = Signet::OAuth2::Client.new(
:authorization_uri => 'https://accounts.google.com/o/oauth2/auth',
:scope => 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/calendar',
# :state => 'useful_dynamic_string', # What is that ?
:redirect_uri => 'http://localhost:3000/auth/google_oauth2/callback',
:client_id => $client_id,
:client_secret => $client_secret
)
redirect_to auth.authorization_uri.to_s
end
def SignetAuth
$code = request['code']
auth = Signet::OAuth2::Client.new(
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
:redirect_uri => 'http://localhost:3000/SignetInsert',
:client_id => $client_id,
:client_secret => $client_secret,
:code => request['code']
)
end
def SignetInsert
auth = Signet::OAuth2::Client.new(
token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
redirect_uri: 'http://localhost:3000/',
# redirect_uri: 'http://localhost:3000/auth/google_oauth2/callback',
:client_id => $client_id,
:client_secret => $client_secret,
:code => $code
)
# puts auth.fetch_access_token!
calendar = Google::Apis::CalendarV3::CalendarService.new
calendar.authorization = auth
calendar_id = 'primary'
#result = calendar.list_events(calendar_id,
max_results: 10,
single_events: true,
order_by: 'startTime',
time_min: Time.now.iso8601)
end
I cant help you much with ruby but i can tell you what "Code was already redeemed' means.
When you authenticate to Google there are three steps.
User is shown a consent form
Assuming the user accepted consent an authentication code is returned to the calling client application.
The calling client exchanges the authentication code for an access token and sometimes a refresh token.
The access token can then be used to access the API. Access tokens expire after an hour and you can use a Refresh token to request a new access token.
The authentication code that you get back as part of the auth flow can only be used once to get the access token and refresh token. "Code was already redeemed' means that you are trying to use a code you have already used.
Like i said i dont know much about ruby but this might help google apis ruby client
Thanks all for your tips and answers, you gave me the way !
It's not that easy, but I did it, with your help, thanks again
So, the answer is multiple :
1/ The refresh token is not served every time, only the first time, this url allow you to dis-authorize your app and have it again in the answer
2/ this article gave some tips on how to start but definitivly down't give you the 'right' way 2
3/ As far as I understood, using a 'clean' request (see step 1)
$auth = Signet::OAuth2::Client.new and you retrieve an authorization_uri, you redirect to (don't forget to_s)
Google display it's form, asking for authorization
- Google redirect the user on your server, you retrieve the access_token and the refresh_token THE FIRST TIME ONLY (see step 1 to de-authorize and having again your refresh_token
-
and, because you (or I) just can't guess it :
$auth.code = request['code']
$auth.grant_type = 'authorization_code'
$token = $auth.fetch_access_token!
you can use your token and so on
it's not that easy but I hope facebook will not be too different
I am using omniauth-oauth2 in rails to authenticate to a site which supports oauth2. After doing the oauth dance, the site gives me the following, which I then persist into the database:
Access Token
Expires_AT (ticks)
Refresh token
Is there an omniauth method to refresh the token automatically after it expires or should I write custom code which to do the same?
If custom code is to be written, is a helper the right place to write the logic?
Omniauth doesn't offer this functionality out of the box so i used the previous answer and another SO answer to write the code in my model User.rb
def refresh_token_if_expired
if token_expired?
response = RestClient.post "#{ENV['DOMAIN']}oauth2/token", :grant_type => 'refresh_token', :refresh_token => self.refresh_token, :client_id => ENV['APP_ID'], :client_secret => ENV['APP_SECRET']
refreshhash = JSON.parse(response.body)
token_will_change!
expiresat_will_change!
self.token = refreshhash['access_token']
self.expiresat = DateTime.now + refreshhash["expires_in"].to_i.seconds
self.save
puts 'Saved'
end
end
def token_expired?
expiry = Time.at(self.expiresat)
return true if expiry < Time.now # expired token, so we should quickly return
token_expires_at = expiry
save if changed?
false # token not expired. :D
end
And before making the API call using the access token, you can call the method like this where current_user is the signed in user.
current_user.refresh_token_if_expired
Make sure to install the rest-client gem and add the require directive require 'rest-client' in the model file. The ENV['DOMAIN'], ENV['APP_ID'] and ENV['APP_SECRET'] are environment variables that can be set in config/environments/production.rb (or development)
In fact, the omniauth-oauth2 gem and its dependency, oauth2, both have some refresh logic built in.
See in https://github.com/intridea/oauth2/blob/master/lib/oauth2/access_token.rb#L80
# Refreshes the current Access Token
#
# #return [AccessToken] a new AccessToken
# #note options should be carried over to the new AccessToken
def refresh!(params = {})
fail('A refresh_token is not available') unless refresh_token
params.merge!(:client_id => #client.id,
:client_secret => #client.secret,
:grant_type => 'refresh_token',
:refresh_token => refresh_token)
new_token = #client.get_token(params)
new_token.options = options
new_token.refresh_token = refresh_token unless new_token.refresh_token
new_token
end
And in https://github.com/intridea/omniauth-oauth2/blob/master/lib/omniauth/strategies/oauth2.rb#L74 :
self.access_token = access_token.refresh! if access_token.expired?
So you may not be able to do it directly with omniauth-oauth2, but you can certainly do something along the lines of this with oauth2:
client = strategy.client # from your omniauth oauth2 strategy
token = OAuth2::AccessToken.from_hash client, record.to_hash
# or
token = OAuth2::AccessToken.new client, token, {expires_at: 123456789, refresh_token: "123"}
token.refresh!
Eero's answer unlocked a path for me to solve this. I have a helper concern for my classes which get me a GmailService. As part of this process, the user object (which contains the google auth info) gets checked if it's expired. If it has, it refreshes before returning the service.
def gmail_service(user)
mail = Google::Apis::GmailV1::GmailService.new
# Is the users token expired?
if user.google_token_expire.to_datetime.past?
oauth = OmniAuth::Strategies::GoogleOauth2.new(
nil, # App - nil seems to be ok?!
"XXXXXXXXXX.apps.googleusercontent.com", # Client ID
"ABC123456" # Client Secret
)
token = OAuth2::AccessToken.new(
oauth.client,
user.google_access_token,
{ refresh_token: user.google_refresh_token }
)
new_token = token.refresh!
if new_token.present?
user.update(
google_access_token: new_token.token,
google_token_expire: Time.at(new_token.expires_at),
google_refresh_token: new_token.refresh_token
)
else
puts("DAMN - DIDN'T WORK!")
end
end
mail.authorization = user.google_access_token
mail
end
There is some information here, too much to list here. It may depend on the provider you are using, and their allowed usage of the refresh-token
Similarly to other answers I followed this approach, where the model storing the auth and refresh tokens is used, abstracting API interactions from that logic.
See https://stackoverflow.com/a/51041855/1392282
If you are using devise you can create a new strategy the following way I guess, so that you don't need to repeat client id and secret everywhere:
# first argument is something called app, but not sure what but nil seems to be fine.
Strategies::MyStrategy.new(nil, *Devise.omniauth_configs[:mystrategy].args)
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:
Signs the user in and retrieves access token for client side along with server code.
Exchanges the server code for access token & refresh token & id token
Creates token pair object that holds access token and refresh token, then stores it in a session hash
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?
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
The line saying:
accesstype => 'offline'
should say:
access_type => 'offline'
I got access for the user using twitter_auth gem. Here is the code for that.
def twitter
client = TwitterOAuth::Client.new(
:consumer_key => '******',
:consumer_secret => '********'
)
request_token = client.request_token(:oauth_callback => new_user_url)
session[:request_token] = request_token
redirect_to request_token.authorize_url
end
def new
client = TwitterOAuth::Client.new(
:consumer_key => '*****',
:consumer_secret => '******'
)
access_token = client.authorize(
session[:request_token].token,
session[:request_token].secret,
:oauth_verifier => params[:oauth_verifier]
)
#For testing purpose, i tried posting a status and its working perfectly fine
client.update('I am authorized')
end
I am confused in using twitter gem cause every example from the docs says:
Twitter.user("sferik").location // throws an error, Twitter::Error::Unauthorized: Invalid / expired Token
From friends and followers
Twitter.accept("sferik") // throws an error, Twitter::Error::Unauthorized: Invalid / expired Token
Twitter.follow("sferik") // throws an error, Twitter::Error::Unauthorized: Invalid / expired Token
All these errors makes sense, cause we are applying these methods on Class not an object. But how to create an object for this. I have a authorized user but how to take actions on his profile using token we got.
Use client instead of Twitter.
You can see here how you should do it.
I've been spending the last few days banging my head against the wall on supporting the ability to add a contact to the Google Contacts API in my Rails 3 application. Despite many false starts, I've finally made some progress by employing the Ruby OAuth gem, and following the tutorial here: http://everburning.com/news/google-analytics-oauth-and-ruby-oh-my/
When I follow this in the console, I get further than I do in my Rails app. I can create an access token, authenticate against Google's service with the specific scope of the Contacts API, and apply the oauth_verifier token to get an access token. But when it comes time to push the data, I get this error:
response = at.post("https://www.google.com/m8/feeds/contacts/default/full", gdata)
=> #<Net::HTTPUnauthorized 401 Unknown authorization header readbody=true>
Where does the "readbody=true" header come from, and how would I get rid of it?
But it's worse in the Rails app. I have one controller action ("googlecontacts") that creates the request token and leads the user to the authentication site with Google:
def googlecontacts
#card = Card.find_by_short_link(params[:id])
#consumer = OAuth::Consumer.new(
'anonymous',
'anonymous',
{
:site => 'https://www.google.com',
:request_token_path => '/accounts/OAuthGetRequestToken',
:access_token_path => '/accounts/OAuthGetAccessToken',
:authorize_path => '/accounts/OAuthAuthorizeToken',
:signature_method => 'HMAC-SHA1',
:oauth_version => '1.0'
})
#request_token = #consumer.get_request_token(
{:oauth_callback => 'http://monkey.dev/cards/google_auth?redir='+#card.short_link},
{:scope => "https://www.google.com/m8/feeds/"}
)
session[:request_token] = #request_token
redirect_to #request_token.authorize_url
end
This appears to work; I get a working request token object, and the user is forwarded to the Google service to authenticate. The callback URL ("google_auth") should take the oauth_verifier token to create an access token. Here's the beginning of the controller:
def google_auth
#access_token = session[:request_token].get_access_token(:oauth_verifier=>params[:oauth_verifier])
And here's where it craps out. The error on that last line is:
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]
But the values that are in there -- the session[:request_token] and the params[:oauth_verifier] -- are present and accounted for in that action! I can't figure out what is nil here.
So I guess I need to figure out this second problem first, but bonus points for answering the first problem as well. :-)
Thanks for reading.
Aaron.
Try setting/getting the session data with a string not symbol, i.e. session["request_token"], not session[:request_token]. I know I've had that issue before in the past.
Unknown authorization header typically means that your signature didn't match what you sent. I do not recommend the oauth gem. It's full of bugs and weird issues and it doesn't properly escape certain parameters.
The Signet gem is the officially supported gem for accessing Google APIs in Ruby.
Here's how you'd implement this with Signet:
require 'signet/oauth_1/client'
require 'addressable/uri'
card = Card.find_by_short_link(params[:id])
callback = Addressable::URI.parse('http://monkey.dev/cards/google_auth')
callback.query_values = {'redir' => card.short_link}
client = Signet::OAuth1::Client.new(
:temporary_credential_uri =>
'https://www.google.com/accounts/OAuthGetRequestToken',
:authorization_uri =>
'https://www.google.com/accounts/OAuthAuthorizeToken',
:token_credential_uri =>
'https://www.google.com/accounts/OAuthGetAccessToken',
:client_credential_key => 'anonymous',
:client_credential_secret => 'anonymous',
:callback => callback
)
session[:temporary_credential] = (
client.fetch_temporary_credential!(:additional_parameters => {
:scope => 'https://www.google.com/m8/feeds/'
})
)
redirect_to(client.authorization_uri)