Skip to content

Instantly share code, notes, and snippets.

@Aupajo
Last active May 11, 2019 14:48
Show Gist options
  • Save Aupajo/6d04954830845c5f47f6 to your computer and use it in GitHub Desktop.
Save Aupajo/6d04954830845c5f47f6 to your computer and use it in GitHub Desktop.
How Restforce handles refreshing OAuth tokens

Summary

You can hook into authentication success by passing an authentication_callback to the client.

If you're happy relying on an internal API call, you can make sure your token is fresh by calling:

restforce_client.authenticate!

Walkthrough

Typical client instantiation looks like this:

client = Restforce.new :oauth_token => 'oauth token', :instance_url  => 'instance url'

Any method to interact with Salesforce (query, find, search, etc.) generally wrap around api_get:

def query(soql)
  response = api_get 'query', :q => soql
  mashify? ? response.body : response.body['records']
end

api_get is defined in Restforce::Concerns::Verbs:

def define_api_verb(verb)
  define_method :"api_#{verb}" do |*args, &block|
    args[0] = api_path(args[0])
    send(verb, *args, &block)
  end
end

send is calling get, which is defined in the same module:

def define_verb(verb)
  define_method verb do |*args, &block|
    retries = options[:authentication_retries]
    begin
      connection.send(verb, *args, &block)
    rescue Restforce::UnauthorizedError
      if retries > 0
        retries -= 1
        connection.url_prefix = options[:instance_url]
        retry
      end
      raise
    end
  end
end

connection comes from Restforce::Concerns::Connection, and is essentially just a Faraday connection automatically configured with a bunch of Middleware:

 def connection
  @connection ||= Faraday.new(options[:instance_url], connection_options) do |builder|
    ...
    # Handles reauthentication for 403 responses.
    builder.use      authentication_middleware, self, options if authentication_middleware
    # Sets the oauth token in the headers.
    builder.use      Restforce::Middleware::Authorization, self, options
    ...
    # Raises errors for 40x responses.
    builder.use      Restforce::Middleware::RaiseError
    ...
  end
end

authentication_middleware is a module that's determined at run time:

def authentication_middleware
  if username_password?
    Restforce::Middleware::Authentication::Password
  elsif oauth_refresh?
    Restforce::Middleware::Authentication::Token
  end
end

Let's look at the middleware.

This simply adds the OAuth header to each request:

def call(env)
  env[:request_headers][AUTH_HEADER] = %(OAuth #{token})
  @app.call(env)
end

def token
  @options[:oauth_token]
end

Restforce::Middleware::RaiseError

This is a type of Faraday::Response middleware (it acts after a response has been made).

def on_complete(env)
  @env = env
  case env[:status]
  when 404
    raise Faraday::Error::ResourceNotFound, message
  when 401
    raise Restforce::UnauthorizedError, message
  when 413
    raise Faraday::Error::ClientError.new("HTTP 413 - Request Entity Too Large", env[:response])
  when 400...600
    raise Faraday::Error::ClientError.new(message, env[:response])
  end
end

The key part to note here is that a Restforce::UnauthorizedError will be raised if a 401 response is returned.

Restforce::Middleware::Authentication::Token

This is an almost empty class, that just defines a params method:

def params
  { :grant_type    => 'refresh_token',
    :refresh_token => @options[:refresh_token],
    :client_id     => @options[:client_id],
    :client_secret => @options[:client_secret] }
end

It inherits from Restforce::Middleware::Authentication, which works like this:

def call(env)
  @app.call(env)
rescue Restforce::UnauthorizedError
  authenticate!
  raise
end

def authenticate!
  response = connection.post '/services/oauth2/token' do |req|
    req.body = encode_www_form(params)
  end
  raise Restforce::AuthenticationError, error_message(response) if response.status != 200
  @options[:instance_url] = response.body['instance_url']
  @options[:oauth_token]  = response.body['access_token']
  @options[:authentication_callback].call(response.body) if @options[:authentication_callback]
  response.body
end

Interestingly, connection in this case refers to a separate Faraday::Connection than we saw previously.

The reraising in the rescue block is for the verb method back in Restforce::Concerns::Verbs:

...
rescue Restforce::UnauthorizedError
  if retries > 0
    retries -= 1
    connection.url_prefix = options[:instance_url]
    retry
  end
  raise
end
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment