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 below, in the
callback handler i.e. in my ExternalApiAuthController#create_social_sharing_auth_account
action request.env['omniauth.auth']
is returned as nil.
A similar issue is found reported in intridea/omniauth repo.
/config/routes.rb
get '/auth/:provider/callback', to: 'external_api_auth#create'
get '/auth/:provider/social_sharing/callback', to: 'external_api_auth#create_social_media_platform_account', as: :social_sharing_auth
get '/auth/failure', to: 'external_api_auth#failure'
/app/controllers/external_api_auth_controller.rb
class ExternalApiAuthController
# GET /auth/failure
def failure
end
# GET /auth/:provider/callback
def create
end
# GET /auth/:provider/social_sharing/callback
def create_social_media_platform_account
end
end
/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_model_obj = MyAnotherModelService.find_by_internal_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',
callback_path: ExternalApiAuthUrl.sharing_auth_callback_path(provider: provider_facebook)
}
end
SETUP_PROC = lambda do |env|
strategy_instance = env['omniauth.strategy']
provider_name = provider_name_from_oauth_strategy_class(strategy_instance.class)
request = Rack::Request.new(env)
is_social_sharing_auth = false
auth_purpose = request.params[ExternalApiAuthUrl::AUTH_PURPOSE_PARAM_NAME]
if ExternalApiAuthUrl.is_auth_purpose_sharing?(auth_purpose: auth_purpose)
is_social_sharing_auth = true
end
opts = case provider_name.downcase.underscore
when 'facebook'
( is_social_sharing_auth ? facebook_opts_for_sharing : facebook_opts )
else
nil
end
if opts.present?
env['omniauth.strategy'].options.merge!(opts)
end
end
OmniAuth.config.logger = Rails.logger
OmniAuth.config.on_failure do |env|
.....
.....
end
Rails.application.config.middleware.use OmniAuth::Builder do
# Reference: https://github.com/intridea/omniauth/wiki/Setup-Phase
provider :facebook, setup: SETUP_PROC
#end
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
line return callback_call if on_callback_path?
always returns false and hence callback phase never get a chance to execute.
Approach 1
To get around this limitation I tried an approach of sending the callback_path
in OmniAuth::Strategies::Facebook.default options
so 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 to OmniAuth::Builder#provider
method invocation:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :facebook, setup: SETUP_PROC, callback_path: ExternalApiAuthUrl.sharing_auth_callback_path(provider: provider_facebook)
end
And to make it work updated SETUP_PROC to look like
SETUP_PROC = lambda do |env|
strategy_instance = env['omniauth.strategy']
provider_name = provider_name_from_oauth_strategy_class(strategy_instance.class)
request = Rack::Request.new(env)
is_social_sharing_auth = false
auth_purpose = request.params[ExternalApiAuthUrl::AUTH_PURPOSE_PARAM_NAME]
if ExternalApiAuthUrl.is_auth_purpose_sharing?(auth_purpose: auth_purpose)
is_social_sharing_auth = true
elsif ( request.path_info.casecmp(ExternalApiAuthUrl.social_sharing_auth_callback_path(provider: provider_name)) == 0 )
is_social_sharing_auth = true
end
opts = case provider_name.downcase.underscore
when 'facebook'
( is_social_sharing_auth ? facebook_opts_for_sharing : facebook_opts )
else
nil
end
unless is_social_sharing_auth
env['omniauth.strategy'].options.delete(:callback_path)
end
if opts.present?
env['omniauth.strategy'].options.merge!(opts)
end
end
However this makes the custom callback_path scenario work but the default callback_path /auth/facebook/callback
scenario fails
because the callback_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.
/app/middleware/omniauth_builder_setup.rb
class OmniauthBuilderSetup
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
Rails.logger.debug ">>>>>>>>>>>>> OmniauthBuilderSetup @app: #{@app.inspect}"
provider_name = provider_name(request.path_info)
unless provider_name
status, headers, response = @app.call(env)
return [status, headers, response]
end
is_social_sharing_auth = false
auth_purpose = request.params[ExternalApiAuthUrl::AUTH_PURPOSE_PARAM_NAME]
if ExternalApiAuthUrl.is_auth_purpose_reviews_social_sharing?(auth_purpose: auth_purpose)
is_social_sharing_auth = true
elsif ( request.path_info.casecmp(ExternalApiAuthUrl.social_sharing_auth_callback_path(provider: provider_name)) == 0 )
is_social_sharing_auth = true
end
if is_social_sharing_auth
middleware_instance = omniauth_strategy_middleware(provider_name, setup: SETUP_PROC, callback_path: ExternalApiAuthUrl.social_sharing_auth_callback_path(provider: provider_name))
else
middleware_instance = omniauth_strategy_middleware(provider_name, setup: SETUP_PROC)
end
Rails.logger.debug ">>>>>>>>>>>>> OmniauthBuilderSetup middleware_instance: #{middleware_instance.inspect}"
@app = middleware_instance
status, headers, response = @app.call(env)
[status, headers, response]
end
private
def provider_name_regex
# matches
# /auth/facebook
# /auth/facebook/callback
# /auth/facebook?auth_purpose=social_sharing
/\A\/auth\/(facebook|twitter)(?:((\/.*)|(\?.+=.+))?)\z/
end
def provider_name(path_info)
match_data = path_info.match(provider_name_regex)
return if match_data.nil?
match_data.captures.first
end
def omniauth_strategy_middleware(klass, *args, &block)
if klass.is_a?(Class)
middleware = klass
else
begin
middleware = OmniAuth::Strategies.const_get("#{OmniAuth::Utils.camelize(klass.to_s)}")
rescue NameError
raise(LoadError.new("Could not find matching strategy for #{klass.inspect}. You may need to install an additional gem (such as omniauth-#{klass})."))
end
end
args.last.is_a?(Hash) ? args.push({}.merge(args.pop)) : args.push({})
middleware.new(middleware, *args, &block)
end
end
/config/application.rb
....
config.middleware.use "OmniauthBuilderSetup"
....
/config/initializers/omniauth.rb (commented out use OmniAuth::Builder
)
....
......
.....
#Rails.application.config.middleware.use OmniAuth::Builder do
# provider :facebook, setup: SETUP_PROC, callback_path: ExternalApiAuthUrl.reviews_social_sharing_auth_callback_path(provider: provider_facebook)
#end
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
. But during callback phase but it fails with following error:
undefined method `call' for OmniAuth::Strategies::Facebook:Class Did you mean? caller
I added few log statements in OmniAuth::Strategy and the following logs were generated.
Started GET "/auth/facebook" for 127.0.0.1 at 2016-07-28 10:28:23 +0530
>>>>>>>>>>>>> OmniauthBuilderSetup @app: #<ActionDispatch::Routing::RouteSet:0x000000073a64c8>
>>>>>>>>>>>>> OmniauthBuilderSetup middleware_instance: #<OmniAuth::Strategies::Facebook>
>>>>>>>>>>>>> OmniauthBuilderSetup @app: #<OmniAuth::Strategies::Facebook>
(facebook) Setup endpoint detected, running now.
(facebook) Request phase initiated.
Started GET "/auth/facebook/callback?code=AQDxel76u_UvtTeSHUw3CzMpA98KTI4V_75qhxV5TGD7rdGcyeCX-FS1nrrlo-EAezZXUPdH9cAC5h4c1xlqoIL7UZ2WLDfXHG4GHWZTEGYHzH7QURNSkrjvDoBNWV90E83f_R6RURl1POsq8ZhmQOFD5YGXRxosiVx4Sof8_vqJZ5UT2S5SFbmVLEtaZZacJDqEbWjNKBrYdrZauuqCS91lEw6Lrz5U5rA2eOmmygAiBwso-cnmOuRu-PptwtIbBL5zw5hPOANQskIFHL-lfbobZYBwy_NsY8Nf-HsJauuymSmtfsQ28UaPlkox9vSinqDAHYhW1ltBXrOX_7P4HfBr&state=3831c127892242fb43aaa2ebfe37cac9e0cd2c8dbea06f3e" for 127.0.0.1 at 2016-07-28 10:28:29 +0530
>>>>>>>>>>>>> OmniauthBuilderSetup @app: #<OmniAuth::Strategies::Facebook>
>>>>>>>>>>>>> OmniauthBuilderSetup middleware_instance: #<OmniAuth::Strategies::Facebook>
>>>>>>>>>>>>> OmniauthBuilderSetup @app: #<OmniAuth::Strategies::Facebook>
>>>>>>>>>>>>> OmniAuth::Strategy call!(env) @app OmniAuth::Strategies::Facebook
>>>>>>>>>>>>> OmniAuth::Strategy call!(env) options #<OmniAuth::Strategy::Options access_token_options=#<OmniAuth::Strategy::Options header_format="OAuth %s" param_name="access_token"> auth_token_params=#<OmniAuth::Strategy::Options> authorize_options=[:scope, :display, :auth_type] authorize_params=#<OmniAuth::Strategy::Options> client_id=nil client_options=#<OmniAuth::Strategy::Options authorize_url="https://www.facebook.com/dialog/oauth" site="https://graph.facebook.com" token_url="oauth/access_token"> client_secret=nil name="facebook" provider_ignores_state=false setup=#<Proc:0x000000065ead70@/jwork/ruby/ror_projects/Reviewgo-JonathanSmith/reviewgo/config/initializers/omniauth.rb:76 (lambda)> skip_info=false token_options=[] token_params=#<OmniAuth::Strategy::Options parse=:query>>
>>>>>>>>>>>>> OmniAuth::Strategy call!(env) class: OmniAuth::Strategies::Facebook
>>>>>>>>>>>>>>OmniAuth::Strategy call!(env) current_path: /auth/facebook/callback
>>>>>>>>>>>>>>OmniAuth::Strategy call!(env) on_callback_path?: true
(facebook) Setup endpoint detected, running now.
(facebook) Callback phase initiated.
NoMethodError (undefined method `call' for OmniAuth::Strategies::Facebook:Class
Did you mean? caller):
app/middleware/omniauth_builder_setup.rb:61:in `call'
If you notice during the callback phase in my middleware the @app holds an instance of OmniAuth::Strategies::Facebook but as soon as the
control reaches to OmniAuth::Strategy @app in OmniAuth::Strategy instance refers to class OmniAuth::Strategies::Facebook
.
>>>>>>>>>>>>> OmniauthBuilderSetup @app: #<OmniAuth::Strategies::Facebook>
>>>>>>>>>>>>> OmniauthBuilderSetup middleware_instance: #<OmniAuth::Strategies::Facebook>
>>>>>>>>>>>>> OmniauthBuilderSetup @app: #<OmniAuth::Strategies::Facebook>
>>>>>>>>>>>>> OmniAuth::Strategy call!(env) @app OmniAuth::Strategies::Facebook
I am sure there is some problem with my middleware. I haven't used middlewares before so don't understand this @app concept. Tried to refer to few resources on web to grasp it but no success.
Can anybody please help me fixing up my middleware so that it can work in desired manner?
If possible please try to make me understand the concept of @app and the status, headers and body values @app.call(env) should return.For e.g. in my case I need the middleware to proceed only if it matches desired omniauth paths. If not it should kind of skip and move ahead without interfering. I don't how to achieve this behavior.
P.S. I am struggling to get around this limitation from past 2 days and with all the details given here, my findings, my approaches I am hoping that somebody from the community will definitely come forward to guide me in resolving my problem.
Thanks.