1

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.

  1. Can anybody please help me fixing up my middleware so that it can work in desired manner?

  2. 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.

Jignesh Gohel
  • 6,236
  • 6
  • 53
  • 89

2 Answers2

1

Wooohhooooooooooooo finally solved it. I fixed the problem in my middleware and it started working. Thanks to this post and its accepted answer which guided me in finding the problem in my middleware and fixing it.

Need to change code in following methods. Below shown is the previous version as is shown in my post above:

  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


  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

Modified code:

  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}"

    status, headers, response = middleware_instance.call(env) # <<<<<--------- Changed here

    [status, headers, response]
  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(@app, *args, &block) # <<<<<--------- Changed here
  end

Update: Created a gist at https://gist.github.com/jiggneshhgohel/4a79aa26cb628533fe132295cffc45b2 containing the whole code. Hope it helps somebody like me and can save time.

Thanks.

Community
  • 1
  • 1
Jignesh Gohel
  • 6,236
  • 6
  • 53
  • 89
0

Ran into this issue and posted a simpler workaround here: https://github.com/omniauth/omniauth/issues/661#issuecomment-630300394

Basically, just use querystring params, which will get passed back to you. Not sure if all providers will do this, but it worked for me, so might be worth a shot.

Anthony Wang
  • 1,285
  • 1
  • 13
  • 14