Stuck with refactoring classes on ruby on rails 5 - ruby-on-rails

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!

Related

Rails Devise token generator with service objects

I am working on a fairly large Rails app which due to large no of activities uses service objects (& light-services gem). Typically actions are performed in steps in the services, with many of the functions/objects used in these steps instantiated in the model it is working with (Customer.rb) in this case.
In one step I am trying to generate a Devise reset_password_token, but hit a glitch
Customer.rb
def self.generate_token
Devise.token_generator
end
and then in service object reset_password.rb
def generate_token
raw, hashed = Devise.token_generator.generate(Customer, :reset_password_token)
producer.reset_password_token = hashed
producer.reset_password_token_sent_at = Time.now.utc
#token = raw
end
All that goes in to db is the timestamp, and a nil, nil for token although token gets put in to the url endpoint so is being generated
If anyone encountered this scenario before, tips more than welcome!
Here is rest of code, not setting the token in DB properly.
def validate_producer_email
self.valid_email = EmailValidator.valid?(producer.email)
end
# #return [TrueClass, FalseClass]
def validate_token
self.producer.reset_password_token = reset_password_token, params['reset_password_token']
customer = Customer.producer.find_by(reset_password_token: reset_password_token)
# true if customer.email && Devise.secure_compare(reset_password_token, params[:reset_password_token])
customer.present?
end
def update_producer
return if ask_underwriter?
Producer::Update
.with(self)
.run(
producer: producer,
force: current_administrator.blank?,
params: {
customer: {
reset_password_token: reset_password_token
}
}
)
end
If anyone has any tips on how to fix?
Thanks

What's the easiest way to make HTTParty return an object/class rather than a JSON response?

Right now I call:
def child(parent_id, child_id, params = {})
if #api_token
self.class.get("/parents/#{parent_id}/children/#{child_id}", query: params,
:headers=>{"Authorization"=>"Token token=#{#api_token}"})
else
self.class.get("/parents/#{parent_id}/children/#{child_id}", query: params)
end
end
It returns the JSON response directly from the API as a hash. Is there an easy way for me to standardize the response so that it parses the JSON and generates a class?
Example:
Original Response
--------
{'child' : { 'name' : 'John Doe', 'age' : 23 } }
Desired Response
--------
res.name # John Doe
res.age # 23
res.class # APIClient::Child
It can be achieved via custom parser passed to request call (however I would strongly advise not to do it and leave it as it is now)
an example of parser you could pass is
class InstanceParser < HTTParty::Parser
def parse
#assuming you always have a json in format { 'one_key_mapping_to_model' => data }
body_as_json = JSON.parse(body) #string parsed to json
model_class_name = body_as_json.keys.first # == 'one_key_mapping'
model_class_data = body_as_json[model_class_name] # == data
class_instance = model_class_name.camelize.constantize.new(model_class_data) # will create new instance of OneKeyMapping
class_instance
end
end
and then in your api call pass self.class.get("/parents/#{parent_id}/children/#{child_id}", query: params, parser: InstanceParser)
Pass the hash to an initializer.
class APIClient::Child
attr_accessor :foo, :bar
def initialize(hash = {})
hash.each do |k,v|
public_send("#{k}=", v)
end
end
end
Then in your API client you would map between responses and objects:
def child(parent_id, child_id, params = {})
opts = { query: params }
opts.merge!(:headers=>{"Authorization"=>"Token token=#{#api_token}"}) if #api_token
begin
res = self.class.get("/parents/#{parent_id}/children/#{child_id}", opts)
case response.code
when 200
APIClient::Child.new(res[:child])
when 404
# not found - raise some sort of error that the API client consumer
# can catch maybe?
else
# probably just log it since there is little we can do here.
end
rescue HTTParty::Error
# probaly just log it. Could be a connection error or something else.
end
end
This is probably a lot less magical than what you have hoped for but what is the role of a API Client if not to map between HTTP requests and objects suitable for consumption. Most of the boooring boilerplate code here when it comes to passing tokens and handling errors can be farmed out parent classes or modules.

Gem to wrap API can't make API key setter work in all classes

I have a Ruby gem which wraps an API. I have two classes: Client and Season with a Configuration module. But I can't access a change to the API Key, Endpoint made via Client in the Season class.
My ApiWrapper module looks like this:
require "api_wrapper/version"
require 'api_wrapper/configuration'
require_relative "api_wrapper/client"
require_relative "api_wrapper/season"
module ApiWrapper
extend Configuration
end
My Configuration module looks like this:
module ApiWrapper
module Configuration
VALID_CONNECTION_KEYS = [:endpoint, :user_agent, :method].freeze
VALID_OPTIONS_KEYS = [:api_key, :format].freeze
VALID_CONFIG_KEYS = VALID_CONNECTION_KEYS + VALID_OPTIONS_KEYS
DEFAULT_ENDPOINT = 'http://defaulturl.com'
DEFAULT_METHOD = :get
DEFAULT_API_KEY = nil
DEFAULT_FORMAT = :json
attr_accessor *VALID_CONFIG_KEYS
def self.extended(base)
base.reset
end
def reset
self.endpoint = DEFAULT_ENDPOINT
self.method = DEFAULT_METHOD
self.user_agent = DEFAULT_USER_AGENT
self.api_key = DEFAULT_API_KEY
self.format = DEFAULT_FORMAT
end
def configure
yield self
end
def options
Hash[ * VALID_CONFIG_KEYS.map { |key| [key, send(key)] }.flatten ]
end
end # Configuration
end
My Client class looks like this:
module ApiWrapper
class Client
attr_accessor *Configuration::VALID_CONFIG_KEYS
def initialize(options={})
merged_options = ApiWrapper.options.merge(options)
Configuration::VALID_CONFIG_KEYS.each do |key|
send("#{key}=", merged_options[key])
end
end
end # Client
end
My Season class looks like this:
require 'faraday'
require 'json'
API_URL = "/seasons"
module ApiWrapper
class Season
attr_accessor *Configuration::VALID_CONFIG_KEYS
attr_reader :id
def initialize(attributes)
#id = attributes["_links"]["self"]["href"]
...
end
def self.all
puts ApiWrapper.api_key
puts ApiWrapper.endpoint
conn = Faraday.new
response = Faraday.get("#{ApiWrapper.endpoint}#{API_URL}/") do |request|
request.headers['X-Auth-Token'] = "ApiWrapper.api_key"
end
seasons = JSON.parse(response.body)
seasons.map { |attributes| new(attributes) }
end
end
end
This is the test I am running:
def test_it_gives_back_a_seasons
VCR.use_cassette("season") do
#config = {
:api_key => 'ak',
:endpoint => 'http://ep.com',
}
client = ApiWrapper::Client.new(#config)
result = ApiWrapper::Season.all
# Make sure we got all season data
assert_equal 12, result.length
#Make sure that the JSON was parsed
assert result.kind_of?(Array)
assert result.first.kind_of?(ApiWrapper::Season)
end
end
Because I set the api_key via the client to "ak" and the endpoint to "http://ep.com" I would expect puts in the Season class's self.all method to print out "ak" and "http://ep.com", but instead I get the defaults set in the Configuration section.
What I am doing wrong?
The api_key accessors you have on Client and on ApiWrapper are independent. You initialize a Client with the key you want, but then Season references ApiWrapper directly. You've declared api_key, etc. accessors in three places: ApiWrapper::Configuration, ApiWrapper (by extending Configuration) and Client. You should probably figure out what your use cases are and reduce that down to being in just one place to avoid confusion.
If you're going to have many clients with different API keys as you make different requests, you should inject the client into Season and use it instead of ApiWrapper. That might look like this:
def self.all(client)
puts client.api_key
puts client.endpoint
conn = Faraday.new
response = Faraday.get("#{client.endpoint}#{API_URL}/") do |request|
request.headers['X-Auth-Token'] = client.api_key
end
seasons = JSON.parse(response.body)
seasons.map { |attributes| new(attributes) }
end
Note that I also replaced the "ApiWrapper.api_key" string with the client.api_key - you don't want that to be a string anyway.
Having to pass client into every request you make is going to get old, so then you might want to pull out something like a SeasonQuery class to hold onto it.
If you're only ever going to have one api_key and endpoint for the duration of your execution, you don't really need the Client as you've set it up so far. Just set ApiWrapper.api_key directly and continue using it in Season.

Refresh token using Omniauth-oauth2 in Rails application

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)

How to check if token has expired

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?

Resources