maestrano/mno-enterprise

View on GitHub
core/lib/mno_enterprise/concerns/controllers/auth/omniauth_callbacks_controller.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# This controller is used to handle the authentication (+creation) of external
# users via OpenID (e.g: QuickBooks OpenID)
#
# When users click on the "sign in with <provider>" button, they get redirected
# to the authorize endpoint (/users/auth/:provider - e.g: /users/auth/intuit).
# The action (handled by parent controller OmniauthCallbacksController) prepares
# the callback url then redirects the user to the OpenID provider (E.g: Intuit)
# for authentication.
#
# Once authentication has been performed at the OpenID provider level (e.g: Intuit)
# the user gets redirected to the callback endpoint (/users/auth/:provider/callback)
# The provider parameter in the url (E.g: intuit) gets automatically redirected to a
# controller action with the same name (handled by parent controller OmniauthCallbacksController)
# as you can see below with intuit.
#
# Then provider specific action then handles the (creation +) authentication of the user.
# Also, it automatically adds the right applications to the user dashboard (e.g: QuickBooks for
# Intuit)
#
# Intuit:
# --------
# For intuit, the authorize endpoint is be bypassed when the user clicks "try Maestrano" from
# the Intuit marketplace. The user automatically lands on the callback endpoints with a parameter
# in the url called 'qb_initiated'. This parameter is used to automatically trigger the retrieval
# of the oauth token in the background via javascript (by storing the temporary grant url in session)
#
# On Intuit, it is also possible to directly choose one of the apps proposed by Maestrano (E.g: 'SugarCRM
# by Maestrano'). In this case, an 'app' attribute containing the app nid (named id - e.g: 'sugarcrm') is
# added to the url parameters. The action then setup the app automatically (along with QuickBooks).
#
#
module MnoEnterprise::Concerns::Controllers::Auth::OmniauthCallbacksController
  extend ActiveSupport::Concern

  #==================================================================
  # Included methods
  #==================================================================
  included do
    skip_filter :verify_authenticity_token, only: [:intuit]

    providers = Devise.omniauth_providers & %i(linkedin google facebook)

    providers.each do |provider|
      provides_callback_for provider
    end
  end

  #==================================================================
  # Class methods
  #==================================================================
  module ClassMethods
    def provides_callback_for(provider)
      class_eval %Q{
        def #{provider}
          auth = env["omniauth.auth"]
          opts = { orga_on_create: create_orga_on_user_creation(auth.info.email) }

          @user = MnoEnterprise::User.find_for_oauth(auth, opts, current_user)

          if @user.persisted?
            sign_in_and_redirect @user, event: :authentication
            set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
          else
            session["devise.#{provider}_data"] = env["omniauth.auth"]
            redirect_to new_user_registration_url
          end
        end
      }
    end
  end

  #==================================================================
  # Instance methods
  #==================================================================
  # GET|POST /users/auth/:action/callback
  def intuit
    auth = request.env['omniauth.auth']
    opts = {
      orga_on_create: create_orga_on_user_creation(auth.info.email),
      authorized_link_to_email: session['omniauth.intuit.authorized_link_to_email']
    }

    # Try to find via intuit
    begin
      @user = MnoEnterprise::User.find_for_oauth(auth, opts, current_user)
    rescue SecurityError
      # Intuit email is NOT a confirmed email. Therefore we need to ask the user to
      # login the old fashion to make sure it is the right user!
      session["omniauth.intuit.request_account_link"] = true
      redirect_to new_user_session_path, notice: "Please sign in using your regular Maestrano account to confirm that you want to link it to your Intuit account"
      return
    end

    # Cleanup any temporary omniauth.intuit session
    cleanup_intuit_session

    if @user && @user.persisted?
      # Automatically adds a QuickBooks app (and any other app passed via :app param)
      # to the user orga
      # Only for new users for which an orga was created (not an invited user
      # typically)
      app_instances = setup_apps(@user,['quickbooks',params[:app]], oauth_keyset: params[:app])
      qb_instance = app_instances.first

      # On Intuit, Mno is configured to add qb_initiated=true if the user
      # comes directly from apps.com (This is a different workflow from using
      # the QuickBooks connect button because we're supposed to trigger the
      # oauth workflow directly via javascript using directConnectToIntuit)
      # Here we store in session the fact that we need to trigger an oauth
      # workflow via directConnectToIntuit
      # ----
      # See layouts/partners/intuit for more info. The session param set
      # below get reset in the view.
      #
      if params[:qb_initiated] && qb_instance && !qb_instance.oauth_keys_valid?
        session[:qb_direct_connect_grant_url] = authorize_webhook_oauth_url(qb_instance.uid)
      end

      # The above methods trigger many different hooks which
      # may impact the user (typically user workspace). It is safer
      # to reload the user before continuing so that the picture
      # is up to date
      @user.reload

      # Check whether we should redirect the user to a specific
      # url
      redirect_url = session.delete(:openid_previous_url) || MnoEnterprise.router.dashboard_path || main_app.root_path

      sign_in @user
      redirect_to redirect_url, event: :authentication

      set_flash_message(:notice, :success, kind: "Intuit") if is_navigational_format?
    else
      session["devise.intuit_data"] = request.env["omniauth.auth"]
      redirect_to home_url, "ng-controller" => "MnoSignupProcessCtrl", "ng-click" => "startProcess()"
    end
  end

  #================================================
  # Private methods
  #================================================
  private

    def cleanup_intuit_session
      session.delete("omniauth.intuit.passthru_email")
      session.delete("omniauth.intuit.request_account_link")
    end

    # Whether to create an orga on user creation
    def create_orga_on_user_creation(user_email = nil)
      return false if user_email.blank?
      return false if MnoEnterprise::User.exists?(email: user_email)

      # First check previous url to see if the user
      # was trying to accept an orga
      if !session[:previous_url].blank? && (r = session[:previous_url].match(/\/orga_invites\/(\d+)\?token=(\w+)/))
        invite_params = { id: r.captures[0].to_i, token: r.captures[1] }
        return false if OrgInvite.where(invite_params).any?
      end

      # Get remaining invites via email address
      return MnoEnterprise::OrgInvite.where(user_email: user_email).empty?
    end

    # Create or find the apps provided in argument
    # Accept an array of app nid (named id - e.g: 'quickbooks')
    # opts:
    #   oauth_keyset: If a oauth_keyset is provided then it will be added to the
    # oauth_keys of any app that is oauth ready (QuickBooks for example)
    #
    # Return an array of app instances (found or created)
    def setup_apps(user = nil, app_nids = [], opts = {})
      return [] unless user
      return [] unless (user.organizations.reload.count == 1)
      return [] unless (org = user.organizations.first)
      return [] unless MnoEnterprise::Ability.new(user).can?(:edit,org)

      results = []

      apps = MnoEnterprise::App.where('nid.in' => app_nids.compact)
      existing = org.app_instances.active.index_by(&:app_id)

      # For each app nid (which is not nil), try to find an existing instance or create one
      apps.each do |app|
        if (app_instance = existing[app.id])
          results << app_instance
        else
          # Provision instance and add to results
          app_instance = org.app_instances.create(product: app.nid)
          results << app_instance
          MnoEnterprise::EventLogger.info('app_add', user.id, 'App added', app_instance)
        end

        # Add oauth keyset if defined and app_instance is
        # oauth ready and does not have a valid set of oauth keys
        if app_instance && opts[:oauth_keyset].present? && !app_instance.oauth_keys_valid?
          app_instance.oauth_keys = { keyset: opts[:oauth_keyset] }
          app_instance.save
        end
      end
      return results
    end
end