lynndylanhurley/devise_token_auth

View on GitHub
app/controllers/devise_token_auth/omniauth_callbacks_controller.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
94%
# frozen_string_literal: true

module DeviseTokenAuth
  class OmniauthCallbacksController < DeviseTokenAuth::ApplicationController
    attr_reader :auth_params

    before_action :validate_auth_origin_url_param

    skip_before_action :set_user_by_token, raise: false
    skip_after_action :update_auth_header

    # intermediary route for successful omniauth authentication. omniauth does
    # not support multiple models, so we must resort to this terrible hack.
    def redirect_callbacks

      # derive target redirect route from 'resource_class' param, which was set
      # before authentication.
      devise_mapping = get_devise_mapping
      redirect_route = get_redirect_route(devise_mapping)

      # preserve omniauth info for success route. ignore 'extra' in twitter
      # auth response to avoid CookieOverflow.
      session['dta.omniauth.auth'] = request.env['omniauth.auth'].except('extra')
      session['dta.omniauth.params'] = request.env['omniauth.params']

      redirect_to redirect_route, {status: 307}.merge(redirect_options)
    end

    def get_redirect_route(devise_mapping)
      path = "#{Devise.mappings[devise_mapping.to_sym].fullpath}/#{params[:provider]}/callback"
      klass = request.scheme == 'https' ? URI::HTTPS : URI::HTTP
      redirect_route = klass.build(host: request.host, port: request.port, path: path).to_s
    end

    def get_devise_mapping
       # derive target redirect route from 'resource_class' param, which was set
       # before authentication.
       devise_mapping = [request.env['omniauth.params']['namespace_name'],
                         request.env['omniauth.params']['resource_class'].underscore.gsub('/', '_')].compact.join('_')
    rescue NoMethodError => err
      default_devise_mapping
    end

    # This method will only be called if `get_devise_mapping` cannot
    # find the mapping in `omniauth.params`.
    #
    # One example use-case here is for IDP-initiated SAML login.  In that
    # case, there will have been no initial request in which to save
    # the devise mapping.  If you are in a situation like that, and
    # your app allows for you to determine somehow what the devise
    # mapping should be (because, for example, it is always the same),
    # then you can handle it by overriding this method.
    def default_devise_mapping
      raise NotImplementedError.new('no default_devise_mapping set')
    end

    def omniauth_success
      get_resource_from_auth_hash
      set_token_on_resource
      create_auth_params

      if confirmable_enabled?
        # don't send confirmation email!!!
        @resource.skip_confirmation!
      end

      sign_in(:user, @resource, store: false, bypass: false)

      @resource.save!

      yield @resource if block_given?

      if DeviseTokenAuth.cookie_enabled
        set_token_in_cookie(@resource, @token)
      end

      render_data_or_redirect('deliverCredentials', @auth_params.as_json, @resource.as_json)
    end

    def omniauth_failure
      @error = params[:message]
      render_data_or_redirect('authFailure', error: @error)
    end

    def validate_auth_origin_url_param
      return render_error_not_allowed_auth_origin_url if auth_origin_url && blacklisted_redirect_url?(auth_origin_url)
    end


    protected

    # this will be determined differently depending on the action that calls
    # it. redirect_callbacks is called upon returning from successful omniauth
    # authentication, and the target params live in an omniauth-specific
    # request.env variable. this variable is then persisted thru the redirect
    # using our own dta.omniauth.params session var. the omniauth_success
    # method will access that session var and then destroy it immediately
    # after use.  In the failure case, finally, the omniauth params
    # are added as query params in our monkey patch to OmniAuth in engine.rb
    def omniauth_params
      unless defined?(@_omniauth_params)
        if request.env['omniauth.params'] && request.env['omniauth.params'].any?
          @_omniauth_params = request.env['omniauth.params']
        elsif session['dta.omniauth.params'] && session['dta.omniauth.params'].any?
          @_omniauth_params ||= session.delete('dta.omniauth.params')
          @_omniauth_params
        elsif params['omniauth_window_type']
          @_omniauth_params = params.slice('omniauth_window_type', 'auth_origin_url', 'resource_class', 'origin')
        else
          @_omniauth_params = {}
        end
      end
      @_omniauth_params
    end

    # break out provider attribute assignment for easy method extension
    def assign_provider_attrs(user, auth_hash)
      attrs = auth_hash['info'].to_hash
      attrs = attrs.slice(*user.attribute_names)
      user.assign_attributes(attrs)
    end

    # derive allowed params from the standard devise parameter sanitizer
    def whitelisted_params
      whitelist = params_for_resource(:sign_up)

      whitelist.inject({}) do |coll, key|
        param = omniauth_params[key.to_s]
        coll[key] = param if param
        coll
      end
    end

    def resource_class(mapping = nil)
      return @resource_class if defined?(@resource_class)

      constant_name = omniauth_params['resource_class'].presence || params['resource_class'].presence
      @resource_class = ObjectSpace.each_object(Class).detect { |cls| cls.to_s == constant_name && cls.pretty_print_inspect.starts_with?(constant_name) }
      raise 'No resource_class found' if @resource_class.nil?

      @resource_class
    end

    def resource_name
      resource_class
    end

    def unsafe_auth_origin_url
      omniauth_params['auth_origin_url'] || omniauth_params['origin']
    end


    def auth_origin_url
      if unsafe_auth_origin_url && blacklisted_redirect_url?(unsafe_auth_origin_url)
        return nil
      end
      return unsafe_auth_origin_url
    end

    # in the success case, omniauth_window_type is in the omniauth_params.
    # in the failure case, it is in a query param.  See monkey patch above
    def omniauth_window_type
      omniauth_params.nil? ? params['omniauth_window_type'] : omniauth_params['omniauth_window_type']
    end

    # this session value is set by the redirect_callbacks method. its purpose
    # is to persist the omniauth auth hash value thru a redirect. the value
    # must be destroyed immediately after it is accessed by omniauth_success
    def auth_hash
      @_auth_hash ||= session.delete('dta.omniauth.auth')
    end

    # ensure that this controller responds to :devise_controller? conditionals.
    # this is used primarily for access to the parameter sanitizers.
    def assert_is_devise_resource!
      true
    end

    def set_random_password
      # set crazy password for new oauth users. this is only used to prevent
      # access via email sign-in.
      p = SecureRandom.urlsafe_base64(nil, false)
      @resource.password = p
      @resource.password_confirmation = p
    end

    def create_auth_params
      @auth_params = {
        auth_token: @token.token,
        client_id:  @token.client,
        uid:        @resource.uid,
        expiry:     @token.expiry,
        config:     @config
      }
      @auth_params.merge!(oauth_registration: true) if @oauth_registration
      @auth_params
    end

    def set_token_on_resource
      @config = omniauth_params['config_name']
      @token  = @resource.create_token
    end

    def render_error_not_allowed_auth_origin_url
      message = I18n.t('devise_token_auth.omniauth.not_allowed_redirect_url', redirect_url: unsafe_auth_origin_url)
      render_data_or_redirect('authFailure', error: message)
    end

    def render_data(message, data)
      @data = data.merge(message: ActionController::Base.helpers.sanitize(message))
      render layout: nil, template: 'devise_token_auth/omniauth_external_window'
    end

    def render_data_or_redirect(message, data, user_data = {})

      # We handle inAppBrowser and newWindow the same, but it is nice
      # to support values in case people need custom implementations for each case
      # (For example, nbrustein does not allow new users to be created if logging in with
      # an inAppBrowser)
      #
      # See app/views/devise_token_auth/omniauth_external_window.html.erb to understand
      # why we can handle these both the same.  The view is setup to handle both cases
      # at the same time.
      if ['inAppBrowser', 'newWindow'].include?(omniauth_window_type)
        render_data(message, user_data.merge(data))

      elsif auth_origin_url # default to same-window implementation, which forwards back to auth_origin_url

        # build and redirect to destination url
        redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true).merge(redirect_options))
      else

        # there SHOULD always be an auth_origin_url, but if someone does something silly
        # like coming straight to this url or refreshing the page at the wrong time, there may not be one.
        # In that case, just render in plain text the error message if there is one or otherwise
        # a generic message.
        fallback_render data[:error] || 'An error occurred'
      end
    end

    def fallback_render(text)
        render inline: %Q(

            <html>
                    <head></head>
                    <body>
                            #{ActionController::Base.helpers.sanitize(text)}
                    </body>
            </html>)
    end

    def handle_new_resource
      @oauth_registration = true
      set_random_password
    end

    def assign_whitelisted_params?
      true
    end

    def get_resource_from_auth_hash
      # find or create user by provider and provider uid
      @resource = resource_class.where(
        uid: auth_hash['uid'],
        provider: auth_hash['provider']
      ).first_or_initialize

      if @resource.new_record?
        handle_new_resource
      end

      # sync user info with provider, update/generate auth token
      assign_provider_attrs(@resource, auth_hash)

      # assign any additional (whitelisted) attributes
      if assign_whitelisted_params?
        extra_params = whitelisted_params
        @resource.assign_attributes(extra_params) if extra_params
      end

      @resource
    end
  end
end