Refresh token using Omniauth-oauth2 in Rails application

冷暖自知 提交于 2019-11-27 11:35:07
ganeshran

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)

Eero

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

Boy, this was harder that it should've been to solve.

Here's the complete, working implementation that we use to provide authorization to google-api-client, which uses Devise, Omniauth, and OAuth2 gems. The GoogleAuthToken class encapsulates knowledge about extracting omniauth info, refreshing itself as necessary, and providing an access token that can be used as authorization for the google-api-client gem.

# Using the OAuth2 gem for auth in `google-api-client` is not quite enough, and requires a mixin
# for the `apply!` method.
# https://github.com/googleapis/google-api-ruby-client/issues/296#issuecomment-147545845
module GoogleAuthorizationOAuth2AccessTokenPatch
  def apply!(headers)
    headers['Authorization'] = "Bearer #{token}"
  end
end

class GoogleAuthToken < ApplicationRecord
  OMNIAUTH_CONFIG_NAME = :google_oauth2

  OAuth2::AccessToken.include GoogleAuthorizationOAuth2AccessTokenPatch

  def self.from_omniauth(omniauth)
    provider, uid, info, credentials = omniauth.provider, omniauth.uid, omniauth.info, omniauth.credentials
    GoogleAuthToken.new(
      uid: uid,
      provider: provider,
      name: info.name,
      email: info.email,
      image: info.image,
      token: credentials.token,
      refresh_token: credentials.refresh_token,
      expires_at: credentials.expires_at,
    )
  end

  def access_token
    return @access_token if @access_token.present? && !@access_token.expired?
    @access_token = build_or_refresh_access_token
  end

  private def build_or_refresh_access_token
    client_id = Devise.omniauth_configs[OMNIAUTH_CONFIG_NAME].strategy.client_id
    client_secret = Devise.omniauth_configs[OMNIAUTH_CONFIG_NAME].strategy.client_secret
    client = Devise.omniauth_configs[OMNIAUTH_CONFIG_NAME].strategy_class.new(nil, client_id, client_secret).client
    access_token = OAuth2::AccessToken.new(client, token, refresh_token: refresh_token, expires_at: expires_at)

    if access_token.expired?
      access_token = access_token.refresh!
      update!(token: access_token.token, refresh_token: access_token.refresh_token, expires_at: access_token.expires_at)
    end

    access_token
  end
end

And then here would be the usage:

# USAGE

require 'google/apis/calendar_v3'

# creating a GoogleAuthToken for the first time, e.g. from the `omniauth_callbacks_controller`
token = GoogleAuthToken.from_omniauth(request.env['omniauth.auth'])
token.save!

# inside of whatever client wrapper or whatever you want
client = Google::Apis::CalendarV3::CalendarService.new
client.authorization = token.access_token
client.list_calendar_lists # or whatever

Obviously you'll want the GoogleAuthToken to belong to something meaningful, e.g. a user, which you can wire up as needed.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!