I'm trying to write a simple OAuth2 client based on oauth2 gem. The idea is to store an access token in a session and check if it's not expired before every request.
The following code for getting a new token looks like that:
client = OAuth2::Client.new(
'26b8e5c92367d703ad35a2fc16b14dc93327a15798068ccba473aa2e3d897883',
'b16079915cdc20b5373f1601e31cece5a84274f772cfd89aec12c90fd110775e',
site: 'http://localhost:3000'
)
client.client_credentials.get_token.expired?
and it's working fine. Request to my api is fired and the last line shows if token has expired or not. The problem is when i'm trying to restore token state by myself:
OAuth2::AccessToken.new(client, session[:api_token]).expired?
This line of code does not fire the request to my api and, in cause of that, has no idea what's that token lifetime, expires_at param or anything else. Everything besides 'token' param is nil so expired? method always returns false:
#<OAuth2::AccessToken:0x007fad4c9e2e28 #client=#<OAuth2::Client:0x007fad4ddb7160 #id="26b8e5c92367d703ad35a2fc16b14dc93327a15798068ccba473aa2e3d897883", #secret="b16079915cdc20b5373f1601e31cece5a84274f772cfd89aec12c90fd110775e", #site="http://localhost:3000", #options={:authorize_url=>"/oauth/authorize", :token_url=>"/oauth/token", :token_method=>:post, :connection_opts=>{}, :connection_build=>nil, :max_redirects=>5, :raise_errors=>true}, #client_credentials=#<OAuth2::Strategy::ClientCredentials:0x007fad4ddb6f80 #client=#<OAuth2::Client:0x007fad4ddb7160 ...>>, #connection=#<Faraday::Connection:0x007fad4ddb6738 #headers={"User-Agent"=>"Faraday v0.8.8"}, #params={}, #options={}, #ssl={}, #parallel_manager=nil, #default_parallel_manager=nil, #builder=#<Faraday::Builder:0x007fad4ddb6620 #handlers=[Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]>, #url_prefix=#<URI::HTTP:0x007fad4ddb60d0 URL:http://localhost:3000/>, #proxy=nil, #app=#<Faraday::Request::UrlEncoded:0x007fad4ddb4190 #app=#<Faraday::Adapter::NetHttp:0x007fad4ddb4280 #app=#<Proc:0x007fad4ddb4370#/usr/local/rvm/gems/ruby-2.0.0-p247/gems/faraday-0.8.8/lib/faraday/connection.rb:93 (lambda)>>>>>, #token="114781bdace77fa7f4629e2b42dbe68ac73326728dddc8102b9c2269e3e86a36", #refresh_token=nil, #expires_in=nil, #expires_at=nil, #options={:mode=>:header, :header_format=>"Bearer %s", :param_name=>"access_token"}, #params={}>
Am i doing something wrong or is that some kind of a bug? To sum it all up: i need to check if token stored in a session (as a string) has expired or not.
If you check the code of AccessToken, you have to pass a third parameter (options) containing "expires_at" value which is used when you are calling expired? :
def initialize(client, token, opts={})
#client = client
#token = token.to_s
[:refresh_token, :expires_in, :expires_at].each do |arg|
instance_variable_set("##{arg}", opts.delete(arg) || opts.delete(arg.to_s))
end
#expires_in ||= opts.delete('expires')
#expires_in &&= #expires_in.to_i
#expires_at &&= #expires_at.to_i
#expires_at ||= Time.now.to_i + #expires_in if #expires_in
#options = {:mode => opts.delete(:mode) || :header,
:header_format => opts.delete(:header_format) || 'Bearer %s',
:param_name => opts.delete(:param_name) || 'access_token'}
#params = opts
end
...
def expired?
expires? && (expires_at < Time.now.to_i)
end
source: https://github.com/intridea/oauth2/blob/master/lib/oauth2/access_token.rb#L42
So update your code to something like:
OAuth2::AccessToken.new(client, session[:api_token], {expires_at: session[:expires_at]}).expired?
Related
I have a class with different methods but on these methods i need to do a check on the access token before doing some calls
class SomeClass
def initialize
#client = SomeModule::Client.new
end
def get_intervention_chart(subId:, projectId:, interventionId:)
#client.check_presence_of_access_token()
SomeModule::Service::Project.new(#client).get_intervention_chart(subId: subId, projectId: projectId, interventionId: interventionId)
end
def get_intervention_documents(subId:, projectId:, interventionId:)
#client.check_presence_of_access_token()
SomeModule::Service::Project.new(#client).get_intervention_documents(subId: subId, projectId: projectId, interventionId: interventionId)
end
end
As you can see, i call the method "check_presence_of_access_token" which check if the access token is there and if it's good to go, if not it gets another one and stock it in a file.
There is my Client class :
class Client
class Configuration
attr_accessor :access_token
attr_reader :access_token_path, :endpoint, :client_id, :client_secret, :subId
def initialize
#access_token = ''
#access_token_path = Rails.root.join('tmp/connection_response.json')
#endpoint = ENV['TOKEN_ENDPOINT']
#client_id = ENV['CLIENT_ID']
#client_secret = ENV['CLIENT_SECRET']
#subId = "SOME_ID"
end
end
def initialize
#configuration = Configuration.new
end
# Check if the file 'connection_response' is present and if the token provided is still valid (only 30 min)
def check_presence_of_access_token
if File.exist?(self.configuration.access_token_path.to_s)
access_token = JSON.parse(File.read(self.configuration.access_token_path.to_s))["access_token"]
if access_token
jwt_decoded = JWT.decode(access_token, nil, false).first
# we want to check if the token will be valid in 60s to avoid making calls with expired token
if jwt_decoded["exp"] > (DateTime.now.to_i + 60)
self.configuration.access_token = access_token
return
end
end
end
get_token()
end
def get_token
config_hash = Hash.new {}
config_hash["grant_type"] = "client_credentials"
config_hash["client_id"] = self.configuration.client_id
config_hash["client_secret"] = self.configuration.client_secret
response = RestClient.post(self.configuration.endpoint, config_hash, headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })
response_body = JSON.parse(response.body)
self.configuration.access_token = response_body["access_token"]
stock_jwt(response_body.to_json)
end
def stock_jwt(response_body)
File.open(self.configuration.access_token_path.to_s, 'w+') do |file|
file.write(response_body)
end
end
end
I don't know how to refactor this, can you help me ?
There is a general principle for OO languages to be "lazy" and defer decisions to as late as possible. We'll use that principle to refactor your code to make the token automatically refresh itself on expiration, and/or fetch itself if it has not already done so.
There is also a group of principles known collectively as SOLID. We'll use those principles also.
The final principle I'll reference is "smaller is better", with respect to methods and modules. Combine that with the "S" (Single Responsibility) from SOLID, and you'll see the refactor contains a lot more, but much smaller methods.
Principles aside, it's not clear from the problem statement that the token is short-lived (lasts only for a "session") or long-lived (eg: lasts longer than a single session).
If the token is long-lived, then storing it into a file is okay, if the only processes using it are on the same system.
If multiple web servers will be using this code, then unless each one is to have its own token, the token should be shared across all of the systems using a data store of some kind, like Redis, MySQL, or Postgres.
Since your code is using a file, we'll assume that multiple processes on the same system might be sharing the token.
Given these principles and assumptions, here is a refactoring of your code, using a file to store the token, using "lazy" deferred, modular logic.
class Client
class Configuration
attr_accessor :access_token
attr_reader :access_token_path, :endpoint, :client_id, :client_secret, :subId
def initialize
#access_token = nil
#access_token_path = Rails.root.join('tmp/connection_response.json')
#endpoint = ENV['TOKEN_ENDPOINT']
#client_id = ENV['CLIENT_ID']
#client_secret = ENV['CLIENT_SECRET']
#sub_id = "SOME_ID"
end
end
attr_accessor :configuration
delegate :access_token, :access_token_path, :endpoint, :client_id, :client_secret, :sub_id,
to: :configuration
TOKEN_EXPIRATION_TIME = 60 # seconds
def initialize
#configuration = Configuration.new
end
# returns a token, possibly refreshed or fetched for the first time
def token
unexpired_token || new_token
end
# returns an expired token
def unexpired_token
access_token unless token_expired?
end
def access_token
# cache the result until it expires
#access_token ||= JSON.parse(read_token)&.fetch("access_token", nil)
end
def read_token
File.read(token_path)
end
def token_path
access_token_path&.to_s || raise("No access token path configured!")
end
def token_expired?
# the token expiration time should be in the *future*
token_expiration_time.nil? ||
token_expiration_time < (DateTime.now.to_i + TOKEN_EXPIRATION_TIME)
end
def token_expiration_time
# cache the token expiration time; it won't change
#token_expiration_time ||= decoded_token&.fetch("exp", nil)
end
def decoded_token
#decoded_token ||= JWT.decode(access_token, nil, false).first
end
def new_token
#access_token = store_token(new_access_token)
end
def store_token(token)
#token_expiration_time = nil # reset cached values
#decoded_token = nil
IO.write(token_path, token)
token
end
def new_access_token
parse_token(request_token_response)
end
def parse_token(response)
JSON.parse(response.body)&.fetch("access_token", nil)
end
def request_token_response
RestClient.post(
endpoint,
credentials_hash,
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' }
)
end
def credentials_hash
{
grant_type: "client_credentials",
client_id: client_id || raise("Client ID not configured!"),
client_secret: client_secret || raise("Client secret not configured!")
}
end
end
So, how does this work?
Assuming that the client code uses the token, just evaluating the token method will cause the unexpired token to be retrieved or refreshed (if it existed), or a new one fetched (if it hadn't existed).
So, there's no need to "check" for the token before using the #client connection. When the #client connection uses the token, the right stuff will happen.
Values that do not change are cached, to avoid having to redo the logic that produced them. Eg: There is no need to decode the JWT string repeatedly.
When the current token time expires, the token_expired? will turn true, causing its caller to return nil, causing that caller to fetch a new_token, which then gets stored.
The great advantage to these small methods are that each can be tested independently, since they each have a very simple purpose.
Good luck with your project!
I am trying to work with the Google Calendar API in my Rails(5.2.1) app but am having real trouble with the refresh token--my understanding of which is tenuous at best, even after having gone through quite a bit of documentation.
Here is my code:
class CalendarsController < ApplicationController
def authorize
client = Signet::OAuth2::Client.new(client_options)
redirect_to client.authorization_uri.to_s
end
def callback
client = Signet::OAuth2::Client.new(client_options)
client.code = params[:code]
response = client.fetch_access_token!
session[:authorization] = response
redirect_to root_url
end
def get_calendars
client = Signet::OAuth2::Client.new(client_options)
client.update!(session[:authorization])
client.update!(
additional_parameters: {
access_type: 'offline',
prompt: 'consent'
}
)
service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = client
# here is my attempt to refresh
begin
service.list_calendar_lists
rescue Google::Apis::AuthorizationError
response = client.refresh!
session[:authorization] = session[:authorization].merge(response)
retry
end
end
def new
all_calendars = get_calendars.items
#calendar_list = all_calendars.select {|calendar| calendar.access_role=="owner"}
end
def client_options
{
client_id: Rails.application.credentials.web[:client_id],
client_secret: Rails.application.credentials.web[:client_secret],
authorization_uri: 'https://accounts.google.com/o/oauth2/auth?access_type=offline&prompt=consent',
token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
scope: Google::Apis::CalendarV3::AUTH_CALENDAR,
redirect_uri: callback_url
}
end
end
If I go to the URL that leads to #authorize I am directed to an OAuth screen and asked for permission to access my calendars. Once granted, the app works as expected. After an hour, the token expires and I can't get it to refresh. You can see my attempt above: Without that attempt, I get a Google::Apis::AuthorizationError. With it, I get "Missing authorization code." I'm totally lost and am having trouble following the documentation.
The documentation is quite challenging, and the error messages don't help much!
You're not showing the client_options that are being passed in, and since everything else looks correct - I'm guessing this is where the problem lies. Are you setting the access_type parameter for offline access, so that you can actually refresh the token without the user having to re-authenticate?
From the documentation:
Set the value to offline if your application needs to refresh access
tokens when the user is not present at the browser. This is the method
of refreshing access tokens described later in this document. This
value instructs the Google authorization server to return a refresh
token and an access token the first time that your application
exchanges an authorization code for tokens.
You can do this in the authorization_uri. For example:
client = Signet::OAuth2::Client.new({
client_id: ...,
client_secret: ...,
authorization_uri: "https://accounts.google.com/o/oauth2/auth?access_type=offline&prompt=consent",
scope: Google::Apis::CalendarV3::AUTH_CALENDAR,
redirect_uri: callback_url
})
Or when calling update!:
client.update!(
additional_parameters: {
access_type: 'offline',
prompt: 'consent'
}
)
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'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.