/config/initializers/omniauth.rb
def provider_facebook
'facebook'
end
def facebook_opts
my_model_obj = MyModelService.find_by_provider_name(provider_facebook)
return unless my_model_obj.present?
app_details_hash = my_model_obj.application_details
client_id = app_details_hash[:client_id]
client_secret = app_details_hash[:client_secret]
return if client_id.blank? || client_secret.blank?
{
client_id: client_id,
client_secret: client_secret,
scope: 'email,manage_pages,publish_pages',
display: 'popup'
}
end
def facebook_opts_for_social_sharing
my_another_model_obj = MyAnotherModelService.find_by_provider_name(provider_facebook)
return unless my_another_model_obj.present?
app_details_hash = my_another_model_obj.application_details
client_id = app_details_hash[:client_id]
client_secret = app_details_hash[:client_secret]
return if client_id.blank? || client_secret.blank?
{
client_id: client_id,
client_secret: client_secret,
scope: 'email,manage_pages,publish_pages',
display: 'popup',
callback_path: ExternalApiAuthUrl.social_sharing_auth_callback_path(provider: provider_facebook)
}
end
def provider_name_from_oauth_strategy_class(oauth_strategy_clazz)
oauth_strategy_clazz.name.split('::').last || ''
end
SETUP_PROC = lambda do |env|
request = Rack::Request.new(env)
is_social_sharing_auth = false
auth_purpose = request.params[ExternalApiAuthUrl::AUTH_PURPOSE_PARAM_NAME]
if ExternalApiAuthUrl.is_auth_purpose_social_sharing?(auth_purpose: auth_purpose)
is_social_sharing_auth = true
end
strategy_instance = env['omniauth.strategy']
provider_name = provider_name_from_oauth_strategy_class(strategy_instance.class)
opts = case provider_name.downcase.underscore
when 'facebook'
( is_social_sharing_auth ? facebook_opts_for_social_sharing : facebook_opts )
else
nil
end
if opts.present?
env['omniauth.strategy'].options.merge!(opts)
end
end
OmniAuth.config.logger = Rails.logger
# References:
# http://www.sitepoint.com/rails-authentication-oauth-2-0-omniauth/
# http://stackoverflow.com/questions/10737200/how-to-rescue-omniauthstrategiesoauth2callbackerror
OmniAuth.config.on_failure do |env|
# Rails.logger.error ">>>>>>>ENV: #{env.inspect}"
strategy_instance = env['omniauth.error.strategy']
provider_name = provider_name_from_oauth_strategy_class(strategy_instance.class)
oauth_strategy_error_instance = env['omniauth.error']
oauth_strategy_error_clazz_name = oauth_strategy_error_instance.class.name
oauth_strategy_error_msg = oauth_strategy_error_instance.message
Rails.logger.error ">>>>>>> #{oauth_strategy_error_clazz_name} error raised with message: #{oauth_strategy_error_msg}"
query_hash = env['rack.request.query_hash']
error = query_hash['error']
error_code = query_hash['error_code']
error_description = query_hash['error_description']
error_reason = query_hash['error_reason']
Rails.logger.error ">>>>>>> #{provider_name} responded with error: #{error}; error_code: #{error_code}; error_description: #{error_description}; error_reason: #{error_reason}"
error_type = env['omniauth.error.type']
new_path = "#{env['SCRIPT_NAME']}#{OmniAuth.config.path_prefix}/failure?provider=#{provider_name}&error_message=#{error_description}&error_code=#{error_code}"
[301, {'Location' => new_path, 'Content-Type' => 'text/html'}, []]
end
Rails.application.config.middleware.use OmniAuth::Builder do
# Reference: https://github.com/intridea/omniauth/wiki/Setup-Phase
provider :facebook, setup: SETUP_PROC
end
/config/routes.rb
get '/auth/:provider/callback', to: 'external_api_auth#create'
get '/auth/:provider/social_sharing/callback', to: 'external_api_auth#create_social_sharing_auth_account', as: :social_sharing_auth
get '/auth/failure', to: 'external_api_auth#failure'
Note:
The callback URLs corresponding to following paths must be registered in the platform-specific apps.
/auth/:provider/callback
for e.g./auth/facebook/callback
/auth/:provider/social_sharing/callback
for e.g./auth/facebook/social_sharing/callback
For e.g. for Facebook under 'Valid OAuth redirect URIs' the following callback urls must be registered
http://localhost:3000/auth/facebook/callback
http://localhost:3000/auth/facebook/social_sharing/callback
/lib/external_api_auth_url.rb
class ExternalApiAuthUrl
AUTH_PURPOSE_PARAM_NAME = "auth_purpose"
FACEBOOK = '/auth/facebook'
GOOGLE = '/auth/google'
TWITTER = '/auth/twitter'
class << self
# Reference: http://stackoverflow.com/a/23272893/936494
include Rails.application.routes.url_helpers
def auth_url(platform_name, query_string_params={})
const_name = platform_name.upcase
return unless self.const_defined?(const_name)
query_string_params ||= {}
url = self.const_get(const_name)
uri = URI.parse(url)
uri.query = URI.encode_www_form(query_string_params) unless query_string_params.blank?
return uri.to_s
end
def auth_url_for_social_sharing(platform_name)
auth_url(platform_name, auth_purpose: 'social_sharing')
end
def is_auth_purpose_social_sharing?(auth_purpose:)
'social_sharing' == auth_purpose
end
def social_sharing_auth_callback_path(provider:)
social_sharing_auth_path(provider: provider)
end
end
end
/app/controllers/external_api_auth_controller.rb
class ExternalApiAuthController < ApplicationController
# GET /auth/failure
def failure
error_code = params[:error_code]
error_message = params[:error_message]
error_string = "#{provider_name} responded with error_code: #{error_code} and error_message: #{error_message}"
Rails.logger.error ">>>>>>>>>>>> #{error_string}"
display_error_string = "#{provider_name} account not connected"
flash.now[:error] = display_error_string
end
# GET /auth/:provider/callback
def create
oauth_info = oauth_info_hash
# Rails.logger.debug ">>>>>>>>>>> #{auth_hash}"
....
.....
end
# GET /auth/:provider/social_sharing/callback
def create_social_sharing_auth_account
render :create
end
protected
def provider_name
params[:provider]
end
def oauth_info_hash
uid = auth_hash[:uid]
credentials_hash = auth_hash[:credentials]
token = credentials_hash[:token]
info_hash = auth_hash[:info]
email = info_hash[:email]
hash = {}
hash.merge!(token: token) if token.present?
hash.merge!(uid: uid) if uid.present?
hash.merge!(email: email) if email.present?
hash
end
def auth_hash
request.env['omniauth.auth']
end
end
Some view file (.haml):
- auth_url = ExternalApiAuthUrl.auth_url_for_social_sharing('facebook')
= link_to(auth_url, class: 'oauth-pop-up', :"data-width" => 600, :"data-height" => 400) do
%strong<
%span.glyphicon.glyphicon-plus(aria-hidden="true")
= "Add Facebook Account For Social Sharing"
Some another view file (.haml):
- auth_url = ExternalApiAuthUrl.auth_url('facebook')
= link_to(auth_url, class: 'oauth-pop-up', :"data-width" => 600, :"data-height" => 400) do
%strong<
%span.glyphicon.glyphicon-plus(aria-hidden="true")
= "Add Facebook Account For Some Other Purpose"
Javascript
(function ($) {
// Reference: http://stackoverflow.com/a/4550743/936494
function popupCenter(url, width, height, name) {
var left = (screen.width/2)-(width/2);
var top = (screen.height/2)-(height/2);
return window.open(url, name, "menubar=no,toolbar=no,status=no,width="+width+",height="+height+",toolbar=no,left="+left+",top="+top);
}
bindClickOnOAuthPopupLink = function() {
$("a.oauth-pop-up").off('click').on('click', function(event) {
var popUpUrl = $(this).attr("href");
var popUpWidth = $(this).attr("data-width");
var popUpHeight = $(this).attr("data-height");
popupCenter(popUpUrl, popUpWidth, popUpHeight, "authPopup");
event.stopPropagation();
return false;
});
};
}) (jQuery)
var ready;
ready = function() {
// Reference: http://stackoverflow.com/questions/4491433/turn-omniauth-facebook-login-into-a-popup
bindClickOnOAuthPopupLink();
};
// References:
// http://stackoverflow.com/questions/18769109/rails-4-turbo-link-prevents-jquery-scripts-from-working
// http://stackoverflow.com/questions/18770517/rails-4-how-to-use-document-ready-with-turbo-links
$(document).ready(ready);
$(document).on('page:load', ready);
Various References which helped:
- https://github.com/intridea/omniauth/wiki/Setup-Phase
- http://stackoverflow.com/questions/9890985/passing-parameters-through-omniauth
- simi/omniauth-facebook#189
- omniauth/omniauth#593
- omniauth/omniauth#730
- http://www.sitepoint.com/rails-authentication-oauth-2-0-omniauth/
- http://stackoverflow.com/questions/10737200/how-to-rescue-omniauthstrategiesoauth2callbackerror
- http://stackoverflow.com/questions/4491433/turn-omniauth-facebook-login-into-a-popup
- http://stackoverflow.com/questions/17257562/overriding-default-scope-set-in-omniauth-gem
Update
Background
My application requires me to connect to two different Facebook apps for different purposes and for which I am using below shown code.
Problem
When using a custom callback_path like in method
facebook_opts_for_social_sharing
shown above, in thecallback handler i.e. in my
ExternalApiAuthController#create_social_sharing_auth_account
actionrequest.env['omniauth.auth']
is returned as nil. A similar issue is found reported in intridea/omniauth repo.With that code what is happening is the
callback_path
is correctly being picked-up during Request Phase.However as soon as the request phase finished and OmniAuth::Strategies::OAuth2#request_phase initiated redirect, only the OmniAuth::Strategies::Facebook.default options are being used by OmniAuth::Strategy instance. As these options don't contain the
callback_path
(after redirect was initiated)on_callback_path?
while evaluating following linereturn callback_call if on_callback_path?
always returns false and hence callback phase never get a chance to execute.Solution Approaches
Approach 1
To get around this limitation I tried an approach of sending the
callback_path
in OmniAuth::Strategies::Facebook.default optionsso that during each phase it gets picked up. Hence instead of passing it through the code in SETUP_PROC like in method
facebook_opts_for_social_sharing
, I passed it in following manner i.e. passed it as an option toOmniAuth::Builder#provider
method invocation:And to make it work updated SETUP_PROC to look like
However this makes the custom callback_path scenario work but the default callback_path
/auth/facebook/callback
scenario fails because thecallback_path
option containing a custom callback_path is always available in OmniAuth::Strategy instance.Approach 2
Thus to get around the limitation posed by Approach 1 I tried another approach of using a middleware which based on the request's path_info and params invokes the strategy middleware with desired options.
/lib/external_api_auth_url.rb
/app/middleware/omniauth_builder_setup.rb
/config/application.rb
/config/initializers/omniauth.rb (Note: commented out
use OmniAuth::Builder
)With this middleware approach the callback phase is initiated in both the scenarios i.e. when using default callback_path
/auth/facebook/callback
and a custom callback_path/auth/facebook/social_sharing/callback
.Helpful References:
My own SO Post: http://stackoverflow.com/questions/38627949/omniauth-custom-callback-path-makes-envomniauth-auth-return-nil-tried-f/38629219#38629219
Thanks.