I have the following code:
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :current_user
def facebook_cookies
#facebook_cookies ||= Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies).symbolize_keys!
end
def current_user
begin
# allow for ?access_token=[TOKEN] for iOS calls.
#access_token = params[:access_token] || facebook_cookies[:access_token]
#graph = Koala::Facebook::API.new(#access_token)
# TODO: move this to session[:current_user]..
#current_user ||= User.from_graph #graph.get_object('me', { fields: 'id,first_name,last_name,gender,birthday' })
rescue
nil # not logged in
end
end
def authenticate
redirect_to(root_url) if current_user.nil?
end
end
(I have setup Koala as described here https://github.com/arsduo/koala/wiki/Koala-on-Rails)
I don't really want to introduce OmniAuth as what I am trying to do is fairly simple. The above code works, the problem is that it is calling Facebook for every page load = not good. I'm guessing I need to store session[:user_id] and then just call User.find(session[:user_id]) for each subsequent request after the user has authenticated?
Can anyone suggest the most efficient way of solving this so I'm not waiting for Facebook on each page load?
You could try something like this:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :current_user, if: Proc.new{ !current_user? }
def facebook_cookies
#facebook_cookies ||= Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies).symbolize_keys!
end
def current_user
begin
# allow for ?access_token=[TOKEN] for iOS calls.
#access_token = params[:access_token] || facebook_cookies[:access_token]
#graph = Koala::Facebook::API.new(#access_token)
# TODO: move this to session[:current_user]..
#current_user ||= User.from_graph #graph.get_object('me', { fields: 'id,first_name,last_name,gender,birthday' })
rescue
nil # not logged in
end
end
def authenticate
redirect_to(root_url) if current_user.nil?
end
def current_user?
!!#current_user
end
end
I have an implementation that is use for my facebook authentication and authorization.
The code is seperated in non state changing methods and statechanging methods.
Feel free to use the code.
## checks if we have access to fb_info and it is not expired
## Non stagechanging
def oauth_is_online
return !(session[:fb_cookies].nil? or session[:fb_cookies]['issued_at'].to_i + session[:fb_cookies]['expires'].to_i < Time.now.to_i - 10)
end
## checks if the user is in the database
## Non stats changing
def oauth_is_in_database
return oauth_is_online && 0 < User.where(:fb_id => session[:fb_cookies]['user_id']).count
end
## Returns true if it is the specified user
## Non stagechanging
def oauth_is_user(user)
return oauth_is_online && session[:fb_cookies]['user_id'] == user.fb_id
end
## Requires the user to be online. If not, hell breaks loose.
def oauth_require_online
if !oauth_ensure_online
render :file => "public/401.html", :status => :unauthorized, :layout => false
return false
end
return session[:fb_cookies]
end
## Requires the user to be online and the correct user. If not, hell breaks loose.
def oauth_require_user(user)
c = oauth_require_online
return false unless c
return c unless !oauth_is_user(user)
render :file => "public/403.html", :status => :unauthorized, :layout => false
return false
end
## Ensures that the user is online
def oauth_ensure_online
begin
# Checks if the user can be verified af online
if !oauth_is_in_database
# the user is not online. Now make sure that they becomes online.
logger.info "Creates new FB Oauth cookies"
fb_cookies = Koala::Facebook::OAuth.new.get_user_info_from_cookie(cookies)
# TODO In the future the session should be reset at this point
# reset_session
session[:fb_cookies_reload] = false
session[:fb_cookies] = fb_cookies
logger.info "Updating user in database."
# Tries to load the user
#current_user = User.where(:fb_id => session[:fb_cookies]['user_id']).first
logger.info "User exists? => #{#current_user.nil?}"
# creating if not found
#current_user = User.new(:fb_id => session[:fb_cookies]['user_id']) unless !#current_user.nil?
# Loading graph
#fb_graph = Koala::Facebook::API.new(session[:fb_cookies]['access_token'])
# Loading info from graph
me = #fb_graph.get_object('me')
#current_user.name_first = me['first_name'].to_s
#current_user.name_last = me['last_name'].to_s
#current_user.email = me['email'].to_s
#current_user.fb_access_token = session[:fb_cookies]['access_token'].to_s
# Saving the updated user
#current_user.save!
return oauth_is_online
else
# the user is online. Load variables.
#current_user = User.where(:fb_id => session[:fb_cookies]['user_id']).first
#fb_graph = Koala::Facebook::API.new(#current_user.fb_access_token)
end
return oauth_is_online
rescue Koala::Facebook::OAuthTokenRequestError => oe
## TODO handle the diferent errors
# user is not online on facebook
# user have not authenticatet us
# token is used allready once.
logger.debug "FB koala OAuthTokenRequestError: #{oe}"
return false
end
end
Related
I am using devise for authentication. I am overwriting devise token generator so that I can use 6 digit code and also overwriting it so that I can support mobile number confirmation.
If a user register with email and OTP is send via email. Registration seems to work fine. A user register with an email. An OTP is sent and after confirmation a user gets confirmed.
But when the user tries to update the email. I am using the same methods to send the confirmation code (as in registration which works fine) the user get saved in unconfirmed_email. A mail gets send in email but after confirmation a user email is not being copied to email field from unconfirmed_email field.
What could be the problem here.
app/services/users/confirmation_code_sender.rb
# frozen_string_literal: true
module Users
class ConfirmationCodeSender
attr_reader :user
def initialize(id:)
#user = User.find(id)
end
# rubocop :disable Metrics/AbcSize
def call
generate_confirmation_token!
if user.email?
DeviseMailer.confirmation_instructions(
user,
user.confirmation_token,
{ to: user.unconfirmed_email || user.email }
).deliver_now
else
Telco::Web::Sms.send_text(recipient: user.unconfirmed_mobile || user.mobile_number, message: sms_text)
end
end
# rubocop :enable Metrics/AbcSize
private
def generate_confirmation_token!
user.confirmation_token = TokenGenerator.token(6)
user.confirmation_sent_at = DateTime.current
user.save!(validate: false)
end
def sms_text
I18n.t('sms.confirmation_token', token: user.confirmation_token)
end
end
end
app/services/users/phone_or_email_updater.rb
# frozen_string_literal: true
module Users
class PhoneOrEmailUpdater < BaseService
def call
authorize!(current_user, to: :user?)
current_user.tap do |user|
user.update!(unconfirmed_mobile: params[:unconfirmed_mobile], unconfirmed_email: params[:unconfirmed_email])
ConfirmationCodeSender.new(id: user.id).call
end
end
end
end
config/nitializers/confirmable.rb
# frozen_string_literal: true
# Overriding this model to support the confirmation for mobile number as well
module Devise
module Models
module Confirmable
def confirm(args = {})
pending_any_confirmation do
return expired_error if confirmation_period_expired?
self.confirmed_at = Time.now.utc
saved = saved(args)
after_confirmation if saved
saved
end
end
def saved(args)
#saved ||= if pending_reconfirmation?
skip_reconfirmation!
save!(validate: true)
else
save!(validate: args[:ensure_valid] == true)
end
end
def pending_reconfirmation?
if unconfirmed_email.present?
self.email = unconfirmed_email
self.unconfirmed_email = nil
true
elsif unconfirmed_mobile.present?
self.mobile_number = unconfirmed_mobile
self.unconfirmed_mobile = nil
true
else
false
end
end
private
def expired_error
errors.add(
:email,
:confirmation_period_expired,
period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
)
false
end
end
end
end
Mobile update seems to be working fine but email is not updating. I am using graphql to update the email
In console I tried using .confirm but it seems to be not working as well the user email is not getting confirmed
In your pending_reconfirmation?, self.unconfirmed_email is assigned to be nil. It seems like pending_reconfirmation? is only called in saved, however, it is called by pending_any_confirmation, too.
https://github.com/heartcombo/devise/blob/8593801130f2df94a50863b5db535c272b00efe1/lib/devise/models/confirmable.rb#L238
# Checks whether the record requires any confirmation.
def pending_any_confirmation
if (!confirmed? || pending_reconfirmation?)
yield
else
self.errors.add(:email, :already_confirmed)
false
end
end
So when the second time the pending_reconfirmation? is called in the saved, pending_reconfirmation? will return false because unconfirmed_email is nil.
You'd better not do actual assignments inside the methods end with ? it will be an implicit side-effect. Maybe create a new method end with ! to change the value of attributes.
For example:
module Devise
module Models
module Confirmable
def confirm(args = {})
pending_any_confirmation do
return expired_error if confirmation_period_expired?
self.confirmed_at = Time.now.utc
saved = saved(args)
after_confirmation if saved
saved
end
end
def saved(args)
#saved ||= if pending_reconfirmation?
reconfirm_email! if unconfirmed_email.present?
reconfirm_mobile! if unconfirmed_mobile.present?
skip_reconfirmation!
save!(validate: true)
else
save!(validate: args[:ensure_valid] == true)
end
end
def pending_reconfirmation?
unconfirmed_email.present? || nconfirmed_mobile.present?
end
def reconfirm_email!
self.email = unconfirmed_email
self.unconfirmed_email = nil
end
def reconfirm_mobile!
self.mobile_number = unconfirmed_mobile
self.unconfirmed_mobile = nil
end
private
def expired_error
errors.add(
:email,
:confirmation_period_expired,
period: Devise::TimeInflector.time_ago_in_words(self.class.confirm_within.ago)
)
false
end
end
end
end
Can you give an advice or recommend some resources related to this topic? I understand how to it in a theory. But I also heard about jwt etc. What are the best practices to implement device/angular/rails role based auth/registration?
The short answer is to read this blog post which goes into details of how the concept is minimally implemented
This would be a long code answer, but I plan to write separate blog post on how to implement it in much more details...
but for now, here is how I implemented it in some project...
First the angular app part, you can use something like Satellizer which plays nicely...
here is the angular auth module in the front-end app
# coffeescript
config = (
$authProvider
$stateProvider
) ->
$authProvider.httpInterceptor = true # to automatically add the headers for auth
$authProvider.baseUrl = "http://path.to.your.api/"
$authProvider.loginRedirect = '/profile' # front-end route after login
$authProvider.logoutRedirect = '/' # front-end route after logout
$authProvider.signupRedirect = '/sign_in'
$authProvider.loginUrl = '/auth/sign_in' # api route for sign_in
$authProvider.signupUrl = '/auth/sign_up' # api route for sign_up
$authProvider.loginRoute = 'sign_in' # front-end route for login
$authProvider.signupRoute = 'sign_up' # front-end route for sign_up
$authProvider.signoutRoute = 'sign_out' # front-end route for sign_out
$authProvider.tokenRoot = 'data'
$authProvider.tokenName = 'token'
$authProvider.tokenPrefix = 'front-end-prefix-in-localstorage'
$authProvider.authHeader = 'Authorization'
$authProvider.authToken = 'Bearer'
$authProvider.storage = 'localStorage'
# state configurations for the routes
$stateProvider
.state 'auth',
url: '/'
abstract: true
templateUrl: 'modules/auth/auth.html'
data:
permissions:
only: ['guest']
redirectTo: 'profile'
.state 'auth.sign_up',
url: $authProvider.signupRoute
views:
'sign_up#auth':
templateUrl: 'modules/auth/sign_up.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
.state 'auth.sign_in',
url: $authProvider.loginRoute
views:
'sign_in#auth':
templateUrl: 'modules/auth/sign_in.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
this is the basic configurations for satellizer... as for the authentication controller... it's something like following
#signIn = (email, password, remember_me) ->
$auth.login
email: email
password: password
remember_me: remember_me
.then(success, error)
return
#signUp = (name, email, password) ->
$auth.signup
name: name
email: email
password: password
.then(success, error)
return
this is the basics for authenticating
as for the backend (RoR API) you should first allow CORS for the front-end app. and add gem 'jwt' to your gemfile.
second implement the API controller and the authentication controller
for example it might look something like the following
class Api::V1::ApiController < ApplicationController
# The API responds only to JSON
respond_to :json
before_action :authenticate_user!
protected
def authenticate_user!
http_authorization_header?
authenticate_request
set_current_user
end
# Bad Request if http authorization header missing
def http_authorization_header?
fail BadRequestError, 'errors.auth.missing_header' unless authorization_header
true
end
def authenticate_request
decoded_token ||= AuthenticationToken.decode(authorization_header)
#auth_token ||= AuthenticationToken.where(id: decoded_token['id']).
first unless decoded_token.nil?
fail UnauthorizedError, 'errors.auth.invalid_token' if #auth_token.nil?
end
def set_current_user
#current_user ||= #auth_token.user
end
# JWT's are stored in the Authorization header using this format:
# Bearer some_random_string.encoded_payload.another_random_string
def authorization_header
return #authorization_header if defined? #authorization_header
#authorization_header =
begin
if request.headers['Authorization'].present?
request.headers['Authorization'].split(' ').last
else
nil
end
end
end
end
class Api::V1::AuthenticationsController < Api::V1::ApiController
skip_before_action :authenticate_user!, only: [:sign_up, :sign_in]
def sign_in
# getting the current user from sign in request
#current_user ||= User.find_by_credentials(auth_params)
fail UnauthorizedError, 'errors.auth.invalid_credentials' unless #current_user
generate_auth_token(auth_params)
render :authentication, status: 201
end
def sign_out
# this auth token is assigned via api controller from headers
#auth_token.destroy!
head status: 204
end
def generate_auth_token(params)
#auth_token = AuthenticationToken.generate(#current_user, params[:remember_me])
end
end
The AuthenticationToken is a model used to keep track of the JWT tokens ( for session management like facebook)
here is the implementation for the AuthenticationToken model
class AuthenticationToken < ActiveRecord::Base
## Relations
belongs_to :user
## JWT wrappers
def self.encode(payload)
AuthToken.encode(payload)
end
def self.decode(token)
AuthToken.decode(token)
end
# generate and save new authentication token for the user
def self.generate(user, remember_me = false)
#auth_token = user.authentication_tokens.create
#auth_token.token = AuthToken.generate(#auth_token.id, remember_me)
#auth_token.save!
#auth_token
end
# check if a token can be used or not
# used by background job to clear the authentication collection
def expired?
AuthToken.decode(token).nil?
end
end
it uses a wrapper called AuthToken which wraps the JWT functionality
here is it's implementation
# wrapper around JWT to encapsulate it's code
# and exception handling and don't polute the AuthenticationToken model
class AuthToken
def self.encode(payload)
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def self.decode(token)
payload = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
rescue JWT::ExpiredSignature
# It will raise an error if it is not a token that was generated
# with our secret key or if the user changes the contents of the payload
Rails.logger.info "Expired Token"
nil
rescue
Rails.logger.warn "Invalid Token"
nil
end
def self.generate(token_id, remember_me = false)
exp = remember_me ? 6.months.from_now : 6.hours.from_now
payload = { id: token_id.to_s, exp: exp.to_i }
self.encode(payload)
end
end
I am trying to authenticate my new Shopify app. First, my authenticate method redirects the shop owner to Shopify's authentication page:
def authenticate
ShopifyAPI::Session.setup({:api_key => "123", :secret => "456"})
session = ShopifyAPI::Session.new("someshop.myshopify.com")
redirect_to session.create_permission_url(["read_orders"], "https://myapp.com/shopify/post_authenticate?user=someshop")
end
Once the shop owner has approved the integration, the redirect uri triggers my post_authenticate method:
def post_authenticate
ShopifyAPI::Session.setup({:api_key => "123", :secret => "456"})
session = ShopifyAPI::Session.new("#{params[:user]}.myshopify.com")
token = session.request_token(:code => params[:code], :signature => params[:signature], :timestamp => params[:timestamp])
end
But the request_token method returns the following error:
#<ShopifyAPI::ValidationException: Invalid Signature: Possible malicious login>
I have read somewhere that you need to be in the same ShopifyAPI session while doing all of this, but it does not say so in the documentation. And the example app takes a very different approach than the documentation.
As per my comment, I utilize the omniauth methodology for authenticating. Here's a gist of the code for reference. https://gist.github.com/agmcleod/7106363317ebd082d3df. Put all the snippets below.
class ApplicationController < ActionController::Base
protect_from_forgery
force_ssl
helper_method :current_shop, :shopify_session
protected
def current_shop
#current_shop ||= Shop.find(session[:shop_id]) if session[:shop_id].present?
end
def shopify_session
if current_shop.nil?
redirect_to new_login_url
else
begin
session = ShopifyAPI::Session.new(current_shop.url, current_shop.token)
ShopifyAPI::Base.activate_session(session)
yield
ensure
ShopifyAPI::Base.clear_session
end
end
end
end
In my login controller:
def create
omniauth = request.env['omniauth.auth']
if omniauth && omniauth[:provider] && omniauth[:provider] == "shopify"
shop = Shop.find_or_create_by_url(params[:shop].gsub(/https?\:\/\//, ""))
shop.update_attribute(:token, omniauth['credentials'].token)
shopify_session = ShopifyAPI::Session.new(shop.url, shop.token)
session[:shop_id] = shop.to_param
redirect_to root_url
else
flash[:error] = "Something went wrong"
redirect_to root_url
end
end
config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :shopify, Settings.api_key, Settings.api_secret,
scope: 'write_products,write_script_tags,read_orders',
setup: lambda { |env| params = Rack::Utils.parse_query(env['QUERY_STRING'])
env['omniauth.strategy'].options[:client_options][:site] = "http://#{params['shop']}" }
end
Then in your routes file, map the create action of your session appropriately:
match '/auth/shopify/callback', :to => 'login#create'
From there i use the shopify_session method as an around filter on the appropriate controllers.
Been trying to get fb_graph working so i can get things like someones friendlist and i cannot get rid of this error. The ActiveSupport::Memoizable is included in the facebook class. Trying figure it out from a fb_graph example application here https://github.com/nov/fb_graph_sample
image of error: http://imgur.com/VXSHhJf
facebook model:
class Facebook < ActiveRecord::Base
def profile
#profile ||= FbGraph::User.me(self.access_token).fetch
end
class << self
extend ActiveSupport::Memoizable
def config
#config ||= if ENV['fb_client_id'] && ENV['fb_client_secret'] && ENV['fb_scope']
{
:client_id => ENV['fb_client_id'],
:client_secret => ENV['fb_client_secret'],
:scope => ENV['fb_scope'],
}
else
YAML.load_file("#{Rails.root}/config/facebook.yml")[Rails.env].symbolize_keys
end
rescue Errno::ENOENT => e
raise StandardError.new("config/facebook.yml could not be loaded.")
end
def app
FbGraph::Application.new config[:client_id], :secret => config[:client_secret]
end
def auth(redirect_uri = nil)
FbGraph::Auth.new config[:client_id], config[:client_secret], :redirect_uri => redirect_uri
end
def identify(fb_user)
_fb_user_ = find_or_initialize_by_identifier(fb_user.identifier.try(:to_s))
_fb_user_.access_token = fb_user.access_token.access_token
_fb_user_.save!
_fb_user_
end
end
end
and here is facebooks_controller
require 'rack/oauth2'
class FacebooksController < ApplicationController
before_filter :require_authentication, :only => :destroy
rescue_from Rack:.center.hero-unit
%h1 Welcome to Dropshare
%h2
This is the home page for Dropshare
%p (at least for time being)
= render 'layouts/facebook_signup'
= render 'layouts/drive_signup'
/
<haml:loud> provide(:title, 'Home')</haml:loud>
<h1>Home</h1>
<p>This is the home page (for the time being) for Dropshare</p>
Sign up now!
:OAuth2::Client::Error, :with => :oauth2_error
# handle Facebook Auth Cookie generated by JavaScript SDK
def show
auth = Facebook.auth.from_cookie(cookies)
authenticate Facebook.identify(auth.user)
redirect_to dashboard_url
end
# handle Normal OAuth flow: start
def new
client = Facebook.auth(callback_facebook_url).client
redirect_to client.authorization_uri(
:scope => Facebook.config[:scope]
)
end
# handle Normal OAuth flow: callback
def create
client = Facebook.auth(callback_facebook_url).client
client.authorization_code = params[:code]
access_token = client.access_token! :client_auth_body
user = FbGraph::User.me(access_token).fetch
authenticate Facebook.identify(user)
redirect_to dashboard_url
end
def destroy
unauthenticate
redirect_to root_url
end
private
def oauth2_error(e)
flash[:error] = {
:title => e.response[:error][:type],
:message => e.response[:error][:message]
}
redirect_to root_url
end
end
Solution
replace
ActiveSupport::Memoizable
with memoist and require 'memoist'
I think you may be running into the fact that ActiveSupport::Memoizable was deprecated & removed from Rails.
https://github.com/rails/rails/commit/36253916b0b788d6ded56669d37c96ed05c92c5c
The author of that gem is running this version of Rails in their gemfile, so I would presume it's supported through this:
gem 'rails', '~>3.2.11'
I'm guessing you're running a newer version of Rails.
I want to integrate Paypal within the Devise user registration process. What I want is to have a standard rails form based on the devise resource, that also has custom fields belonging to the user's model.
When a user fills in those fields and clicks on signup, it will be redirected to Paypal, when he clears from paypal and returns to our site then the user data must be created.
For the scenario where the user fill's out the paypal form but doesn't come back to our site, we have to keep record of user before redirecting to Paypal.
For this we can create a flag in user model and use Paypal IPN and when the user transaction notified, set that flag.
But in the case when the user is redirected to Paypal but doesn't complete the transaction, if the user returns to registration and signup again, our model should not throw error saying that the email entered already exists in the table.
How can we handle all these scenarios, is there any gem or plugin available to work with?
Here i am posting the detail code for performing the whole process.
registration_controller.rb
module Auth
class RegistrationController < Devise::RegistrationsController
include Auth::RegistrationHelper
def create
#user = User.new params[:user]
if #user.valid?
redirect_to get_subscribe_url(#user, request)
else
super
end
end
end
end
registration_helper.rb
module Auth::RegistrationHelper
def get_subscribe_url(user, request)
url = Rails.env == "production" ? "https://www.paypal.com/cgi-bin/webscr/?" : "https://www.sandbox.paypal.com/cgi-bin/webscr/?"
url + {
:ip => request.remote_ip,
:cmd => '_s-xclick',
:hosted_button_id => (Rails.env == "production" ? "ID_FOR_BUTTON" : "ID_FOR_BUTTON"),
:return_url => root_url,
:cancel_return_url => root_url,
:notify_url => payment_notifications_create_url,
:allow_note => true,
:custom => Base64.encode64("#{user.email}|#{user.organization_type_id}|#{user.password}")
}.to_query
end
end
payment_notification_controller.rb
class PaymentNotificationsController < ApplicationController
protect_from_forgery :except => [:create]
layout "single_column", :only => :show
def create
#notification = PaymentNotification.new
#notification.transaction_id = params[:ipn_track_id]
#notification.params = params
#notification.status = "paid"
#custom = Base64.decode64(params[:custom])
#custom = #custom.split("|")
#user = User.new
#user.email = #custom[0]
#user.organization_type_id = #custom[1].to_i
#user.password = #custom[2]
if #user.valid?
#user.save
#notification.user = #user
#notification.save
#user.send_confirmation_instructions
end
render :nothing => true
end
def show
end
end