lib/oauth2/authenticator.rb



# frozen_string_literal: true

require "base64"

module OAuth2
  # Builds and applies client authentication to token and revoke requests.
  #
  # Depending on the selected mode, credentials are applied as Basic Auth
  # headers, request body parameters, or only the client_id is sent (TLS).
  class Authenticator
    include FilteredAttributes

    # @return [Symbol, String] Authentication mode (e.g., :basic_auth, :request_body, :tls_client_auth, :private_key_jwt)
    # @return [String, nil] Client identifier
    # @return [String, nil] Client secret (filtered in inspected output)
    attr_reader :mode, :id, :secret
    filtered_attributes :secret

    # Create a new Authenticator
    #
    # @param [String, nil] id Client identifier
    # @param [String, nil] secret Client secret
    # @param [Symbol, String] mode Authentication mode
    def initialize(id, secret, mode)
      @id = id
      @secret = secret
      @mode = mode
    end

    # Apply the request credentials used to authenticate to the Authorization Server
    #
    # Depending on the configuration, this might be as request params or as an
    # Authorization header.
    #
    # User-provided params and header take precedence.
    #
    # @param [Hash] params a Hash of params for the token endpoint
    # @return [Hash] params amended with appropriate authentication details
    def apply(params)
      case mode.to_sym
      when :basic_auth
        apply_basic_auth(params)
      when :request_body
        apply_params_auth(params)
      when :tls_client_auth
        apply_client_id(params)
      when :private_key_jwt
        params
      else
        raise NotImplementedError
      end
    end

    # Encodes a Basic Authorization header value for the provided credentials.
    #
    # @param [String] user The client identifier
    # @param [String] password The client secret
    # @return [String] The value to use for the Authorization header
    def self.encode_basic_auth(user, password)
      "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
    end

  private

    # Adds client_id and client_secret request parameters if they are not
    # already set.
    #
    # @param [Hash] params Request parameters
    # @return [Hash] Updated parameters including client_id and client_secret
    def apply_params_auth(params)
      result = {}
      result["client_id"] = id unless id.nil?
      result["client_secret"] = secret unless secret.nil?
      result.merge(params)
    end

    # When using schemes that don't require the client_secret to be passed (e.g., TLS Client Auth),
    # we don't want to send the secret
    #
    # @param [Hash] params Request parameters
    # @return [Hash] Updated parameters including only client_id
    def apply_client_id(params)
      result = {}
      result["client_id"] = id unless id.nil?
      result.merge(params)
    end

    # Adds an `Authorization` header with Basic Auth credentials if and only if
    # it is not already set in the params.
    #
    # @param [Hash] params Request parameters (may include :headers)
    # @return [Hash] Updated parameters with Authorization header
    def apply_basic_auth(params)
      headers = params.fetch(:headers, {})
      headers = basic_auth_header.merge(headers)
      params.merge(headers: headers)
    end

    # Build the Basic Authorization header.
    #
    # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
    # @return [Hash] Header hash containing the Authorization entry
    def basic_auth_header
      {"Authorization" => self.class.encode_basic_auth(id, secret)}
    end
  end
end