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
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 working on a project to do CRUD Operations to firebase. I made use of this to help facilitate and link my ruby project to firebase.
Functions:
def delete_firebase(event_params,rootpath="Events/")
query = init_firebase.delete(rootpath,event_params)
end
def new_firebase(event_params,rootpath="Events")
query = init_firebase.push(rootpath,event_params)
end
def init_firebase # Inits firebase project with URL and secret
firebaseURL = "myfirebaseprojecturl"
firebaseSecret = "myfirebasesecret"
firebase = Firebase::Client.new(firebaseURL, firebaseSecret)
end
Event params consist of my event parameters as shown below
def event_params
params.require(:event).permit(:eventID, :eventName, :attachment, :eventNoOfPpl, :eventAdminEmail, {eventpics: []})
end
I encountered an issue. When I push with push() into firebase, there is a random key like -LSFOklvcdmfPOWrxgBo. In such case, the structure of the document would look like this:
But I cannot delete anything from -LSFOklvcdmfPOWrxgBo as I do not have the value. I used delete() from Oscar's firebase-ruby gem. I would appreciate any help with this issue.
I re-read the gem docs, and got some help from my friends and came up with two solutions
The body's response has response.body # => { 'name' => "-INOQPH-aV_psbk3ZXEX" } and thus, you're able to find out the name if you'd like
Change the index, and don't use .push, instead I made use of .set and did a random number for every event
Final solution
def load_firebase(root_path = "Events")
firebase_json = init_firebase.get(root_path)
if valid_json?(firebase_json.raw_body)
#json_object = JSON.parse(firebase_json.raw_body)
end
end
def update_firebase(event_params, root_path = "Events/")
init_firebase.update("#{root_path}#{event_params["eventID"]}", event_params)
end
def delete_firebase(event_params, root_path = "Events/")
init_firebase.delete("#{root_path}#{event_params["eventID"]}")
end
def save_firebase(event_params, root_path = "Events/")
init_firebase.set("#{root_path}#{event_params["eventID"]}", event_params)
end
I'm tryig to get the user address from facebook with Omniauth but did not work.
i added their address on update callback after login.
If i removed their address from omniauth the app did not update their address.
Someone have any idea how to get their address and why the app did not edit and update their address after the login?
thank's
def omniauth_callback
auth_hash = request.env['omniauth.auth']
user = User.find_by_uid(auth_hash[:uid])
if user.nil? && auth_hash[:info][:email]
user = User.find_by_email(auth_hash[:info][:email])
end
if user.nil?
email_domain = ''
email_domain = '#facebook.com' if auth_hash[:provider] == 'facebook'
user = User.new(email: auth_hash[:info][:email] || auth_hash[:info][:nickname] + email_domain, name: auth_hash[:info][:first_name] || '', surname: auth_hash[:info][:last_name] || '', gender: 'I')
user.password_digest = ''
user.save!(validate: false)
end
user.update_attribute(:login_at, Time.zone.now)
user.update_attribute(:address)
user.update_attribute(:neighborhood)
user.update_attribute(:postal_code)
user.update_attribute(:ip_address, request.remote_ip)
user.update_attribute(:provider, auth_hash[:provider])
user.update_attribute(:uid, auth_hash[:uid])
user.update_attribute(:oauth_token, auth_hash[:credentials][:token])
user.update_attribute(:oauth_expires_at, Time.at(auth_hash[:credentials][:expires_at]))
cookies[:auth_token] = { value: user.oauth_token, expires: user.oauth_expires_at}
redirect_to root_url
end
One reason your code will not work is because this
user.update_attribute(:address)
Doesn't do anything - except raise an error. You have to pass a value into update_attribute as well as specify the field.
Also as #mysmallidea points out, you'd be better advised to use update as that will allow you to update multiple fields in one database action.
If present, the address data will be within the auth_hash. So I suggest that you first work out the structure of that hash. In your development environment, add the following:
Rails.logger.info auth_hash.inspect
That will output the current auth_hash to logs/development.log. Use that to determine where in the hash the address data is. You'll then be able to do something like:
user.update address: auth_hash[:info][:address]
However, you may find that the address is not included in the data returned by the facebook oauth system. In which case you will need to return to their documentation to see if this is possible.
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)
So I have some code here I need to modify regarding a Rails Sweeper:
class UserTrackingSweeper < ActionController::Caching::Sweeper
observe User
def after_update(user)
return if user.nil? || user.created_at.nil? #fix weird bug complaining about to_date on nil class
return if user.created_at.to_date < Date.today || user.email.blank?
user.send_welcome_email if user.email_was.blank?
end
#use sweeper as a way to ingest metadata from the user access to the site automatically
def after_create(user)
begin
if !cookies[:user_tracking_meta].nil?
full_traffic_source = cookies[:user_tracking_meta]
else
if !session.empty? && !session[:session_id].blank?
user_tracking_meta = Rails.cache.read("user_meta_data#{session[:session_id]}")
full_traffic_source = CGI::unescape(user_tracking_meta[:traffic_source])
end
end
traffic_source = URI::parse(full_traffic_source).host || "direct"
rescue Exception => e
Rails.logger.info "ERROR tracking ref link. #{e.message}"
traffic_source = "unknown"
full_traffic_source = "unknown"
end
# if I am registered from already, than use that for now (false or null use site)
registered_from = user.registered_from || "site"
if params && params[:controller]
registered_from = "quiz" if params[:controller].match(/quiz/i)
# registered_from = "api" if params[:controller].match(/api/i)
end
meta = {
:traffic_source => user.traffic_source || traffic_source,
:full_traffic_source => full_traffic_source,
:registered_from => registered_from,
:id_hash => user.get_id_hash
}
user.update_attributes(meta)
end
end
The problem is I've noticed that it dosen't seem possible to access the cookies and parameters hash within a sweeper yet it appears fine in some of our company's integration environments. It does not work in my local machine though. So my questions are:
How is it possible to access params / cookies within a Sweeper?
If it's not possible, what would you do instead?
Thanks
I'm sure you can use session variables in a Cache Sweeper so if anything put whatever you need there and you're set