lib/omniauth/strategies/oidc/callback.rb



# frozen_string_literal: true

module OmniAuth
  module Strategies
    class Oidc
      # Callback phase
      module Callback
        def callback_phase # rubocop:disable Metrics
          error = params["error_reason"] || params["error"]
          error_description = params["error_description"] || params["error_reason"]
          invalid_state = (options.require_state && params["state"].to_s.empty?) || params["state"] != stored_state

          raise CallbackError, error: params["error"], reason: error_description, uri: params["error_uri"] if error
          raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state

          return unless valid_response_type?

          options.issuer = issuer if options.issuer.nil? || options.issuer.empty?

          verify_id_token!(params["id_token"]) if configured_response_type == "id_token"

          client.redirect_uri = redirect_uri

          return id_token_callback_phase if configured_response_type == "id_token"

          client.authorization_code = authorization_code

          access_token
          super
        rescue CallbackError => e
          fail!(e.error, e)
        rescue ::Rack::OAuth2::Client::Error => e
          fail!(e.response[:error], e)
        rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
          fail!(:timeout, e)
        rescue ::SocketError => e
          fail!(:failed_to_connect, e)
        end

        private

        def access_token
          return @access_token if @access_token

          token_request_params = {
            scope: (scope if options.send_scope_to_token_endpoint),
            client_auth_method: options.client_auth_method
          }

          if options.pkce
            token_request_params[:code_verifier] =
              params["code_verifier"] || session.delete("omniauth.pkce.verifier")
          end

          set_client_options_for_callback_phase

          @access_token = client.access_token!(token_request_params)

          verify_id_token!(@access_token.id_token) if configured_response_type == "code"

          options.fetch_user_info ? user_info_from_access_token : define_access_token
        end

        def id_token_callback_phase
          user_data = decode_id_token(params["id_token"]).raw_attributes

          define_user_info(user_data)
        end

        def valid_response_type?
          return true if params.key?(configured_response_type)

          error_attrs = RESPONSE_TYPE_EXCEPTIONS[configured_response_type]
          fail!(error_attrs[:key], error_attrs[:exception_class].new(params["error"]))

          false
        end

        def user_info_from_access_token
          user_data = HTTParty.get(
            config.userinfo_endpoint, {
              headers: {
                "Authorization" => "Bearer #{@access_token}",
                "Content-Type" => "application/json"
              }
            }
          )

          define_user_info(user_data.parsed_response)
        end

        def define_user_info(user_data)
          env["omniauth.auth"] = AuthHash.new(
            provider: name,
            uid: user_data["sub"],
            info: { name: user_data["name"], email: user_data["email"] },
            extra: { raw_info: user_data },
            credentials: {
              id_token: @access_token.id_token,
              token: @access_token.access_token,
              refresh_token: @access_token.refresh_token,
              expires_in: @access_token.expires_in,
              scope: @access_token.scope
            }
          )
          call_app!
        end

        def define_access_token
          env["omniauth.auth"] = AuthHash.new(
            provider: name,
            credentials: {
              id_token: @access_token.id_token,
              token: @access_token.access_token,
              refresh_token: @access_token.refresh_token,
              expires_in: @access_token.expires_in,
              scope: @access_token.scope
            }
          )
          call_app!
        end

        def configured_response_type
          @configured_response_type ||= options.response_type.to_s
        end

        # Parse response from OIDC endpoint and set client options for callback phase
        def set_client_options_for_callback_phase
          client.host = host
          client.redirect_uri = redirect_uri
          client.authorization_endpoint = resolve_endpoint_from_host(host, config.authorization_endpoint)
          client.token_endpoint = resolve_endpoint_from_host(host, config.token_endpoint)
          client.userinfo_endpoint = resolve_endpoint_from_host(host, config.userinfo_endpoint)
        end
      end
    end
  end
end