Skip to content

Instantly share code, notes, and snippets.

@jiggneshhgohel
Last active October 8, 2021 10:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jiggneshhgohel/4a79aa26cb628533fe132295cffc45b2 to your computer and use it in GitHub Desktop.
Save jiggneshhgohel/4a79aa26cb628533fe132295cffc45b2 to your computer and use it in GitHub Desktop.
Omniauth Dynamic Setup Custom Params Custom Callback

/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")
        &nbsp;
        = "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")
        &nbsp;
        = "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:

@jiggneshhgohel
Copy link
Author

jiggneshhgohel commented Jul 28, 2016

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

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.

Solution Approaches

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.

/lib/external_api_auth_url.rb

  class ExternalApiAuthUrl
    ...
    ...

    class << self
      ...
      ...
      ...

      def social_sharing_auth_callback_path(provider:)
        # Note: When this method is invoked from middleware /app/middleware/omniauth_builder_setup.rb
        # the url helper (commented-out below) is unavailable hence using the hardcoded path.
        # social_sharing_auth_path(provider: provider)

        # Note: This path must match with the one defined in config/routes.rb
        "/auth/:provider/social_sharing/callback".gsub(/:provider/, provider)
      end

      def is_social_sharing_auth_request?(provider_name:, request_obj:)
        return false if provider_name.blank? || request_obj.blank?

        auth_purpose = request_obj.params[AUTH_PURPOSE_PARAM_NAME]

        if auth_purpose.present?
          return is_auth_purpose_social_sharing?(auth_purpose: auth_purpose)
        end

        request_path_info = request_obj.path_info

        if request_path_info.present?
          callback_path = social_sharing_auth_callback_path(provider: provider_name)
          return callback_path.casecmp(request_path_info) == 0
        end

        false
      end
    end
  end

/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 = ExternalApiAuthUrl.is_social_sharing_auth_request?(provider_name: provider_name, request_obj: request)

        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)

        [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(@app, *args, &block)
      end
    end

/config/application.rb

  ....

    config.middleware.use "OmniauthBuilderSetup"
  ....

/config/initializers/omniauth.rb (Note: commented out use OmniAuth::Builder)

  ....
  ....
  ....

  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 = ExternalApiAuthUrl.is_social_sharing_auth_request?(provider_name: provider_name, request_obj: request)

    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

  .....
  ....
  ....

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment