32

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:

  1. Access Token
  2. Expires_AT (ticks)
  3. 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?

Eero
  • 4,704
  • 4
  • 37
  • 40
ganeshran
  • 3,512
  • 7
  • 41
  • 69

6 Answers6

26

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)

James Chevalier
  • 10,604
  • 5
  • 48
  • 74
ganeshran
  • 3,512
  • 7
  • 41
  • 69
24

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!
Bryan Ash
  • 4,385
  • 3
  • 41
  • 57
Eero
  • 4,704
  • 4
  • 37
  • 40
  • 2
    Is there an easy way to load the client from an existing strategy in a Rails app? I ended up declaring a new client just as the Oauth2 strategy does: https://github.com/intridea/omniauth-oauth2/blob/master/lib/omniauth/strategies/oauth2.rb#L34 – bgentry Apr 23 '14 at 22:50
  • 1
    I guess your question is, how do I find a reference to the existing OmniAuth strategy? Once you have the strategy object, it has a `client` method. Here's other people asking about finding the strategies: http://stackoverflow.com/questions/13112430/find-loaded-providers-for-omniauth – Eero Apr 24 '14 at 12:39
  • 1
    Thanks @Eero - this led me to my fix below: http://stackoverflow.com/a/38276378/224707 – Nick Jul 08 '16 at 23:07
7

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
Nick
  • 2,803
  • 1
  • 39
  • 59
  • 1
    OmniAuth::Strategies::YOUR_PROVIDER(nil, client_id, secret)... is this the only way to access to strategies, and then their client? – kukrt Jul 28 '17 at 05:32
5

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

Shane O'Grady
  • 2,465
  • 18
  • 20
0

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

Nuno Silva
  • 728
  • 11
  • 27
0

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)
radalin
  • 600
  • 5
  • 13