# frozen_string_literal: true

module Doorkeeper
  class TokensController < Doorkeeper::ApplicationMetalController
    before_action :validate_presence_of_client, only: [:revoke]

    rescue_from Errors::DoorkeeperError do |e|
      handle_token_exception(e)
    end

    def create
      headers.merge!(authorize_response.headers)
      render json: authorize_response.body,
             status: authorize_response.status
    end

    # OAuth 2.0 Token Revocation - https://datatracker.ietf.org/doc/html/rfc7009
    def revoke
      # The authorization server responds with HTTP status code 200 if the client
      # submitted an invalid token or the token has been revoked successfully.
      if token.blank?
        render json: {}, status: 200
      # The authorization server validates [...] and whether the token
      # was issued to the client making the revocation request. If this
      # validation fails, the request is refused and the client is informed
      # of the error by the authorization server as described below.
      elsif authorized?
        revoke_token
        render json: {}, status: 200
      else
        render json: revocation_error_response, status: :forbidden
      end
    end

    # OAuth 2.0 Token Introspection - https://datatracker.ietf.org/doc/html/rfc7662
    def introspect
      introspection = OAuth::TokenIntrospection.new(server, token)

      if introspection.authorized?
        render json: introspection.to_json, status: 200
      else
        error = introspection.error_response
        headers.merge!(error.headers)
        render json: error.body, status: error.status
      end
    end

    private

    def validate_presence_of_client
      return if Doorkeeper.config.skip_client_authentication_for_password_grant

      # @see 2.1.  Revocation Request
      #
      #  The client constructs the request by including the following
      #  parameters using the "application/x-www-form-urlencoded" format in
      #  the HTTP request entity-body:
      #     token   REQUIRED.
      #     token_type_hint  OPTIONAL.
      #
      #  The client also includes its authentication credentials as described
      #  in Section 2.3. of [RFC6749].
      #
      #  The authorization server first validates the client credentials (in
      #  case of a confidential client) and then verifies whether the token
      #  was issued to the client making the revocation request.
      return if server.client

      # If this validation [client credentials / token ownership] fails, the request is
      # refused and the  client is informed of the error by the authorization server as
      # described below.
      #
      # @see 2.2.1.  Error Response
      #
      # The error presentation conforms to the definition in Section 5.2 of [RFC6749].
      render json: revocation_error_response, status: :forbidden
    end

    # OAuth 2.0 Section 2.1 defines two client types, "public" & "confidential".
    #
    # RFC7009
    # Section 5. Security Considerations
    # A malicious client may attempt to guess valid tokens on this endpoint
    # by making revocation requests against potential token strings.
    # According to this specification, a client's request must contain a
    # valid client_id, in the case of a public client, or valid client
    # credentials, in the case of a confidential client. The token being
    # revoked must also belong to the requesting client.
    #
    # Once a confidential client is authenticated, it must be authorized to
    # revoke the provided access or refresh token. This ensures one client
    # cannot revoke another's tokens.
    #
    # Doorkeeper determines the client type implicitly via the presence of the
    # OAuth client associated with a given access or refresh token. Since public
    # clients authenticate the resource owner via "password" or "implicit" grant
    # types, they set the application_id as null (since the claim cannot be
    # verified).
    #
    # https://datatracker.ietf.org/doc/html/rfc6749#section-2.1
    # https://datatracker.ietf.org/doc/html/rfc7009
    def authorized?
      # Token belongs to specific client, so we need to check if
      # authenticated client could access it.
      if token.application_id? && token.application.confidential?
        # We authorize client by checking token's application
        server.client && server.client.application == token.application
      else
        # Token was issued without client, authorization unnecessary
        true
      end
    end

    def revoke_token
      # The authorization server responds with HTTP status code 200 if the token
      # has been revoked successfully or if the client submitted an invalid
      # token
      revocable_token.revoke if revocable_token.revocable?
    end

    def token
      revocable_token&.token
    end

    def revocable_token
      return @revocable_token if defined? @revocable_token

      @revocable_token =
        if params[:token_type_hint] == "refresh_token"
          refresh_token
        else
          access_token || refresh_token
        end
    end

    def refresh_token
      token = Doorkeeper.config.access_token_model.by_refresh_token(params["token"])
      return unless token

      RevocableTokens::RevocableRefreshToken.new(token)
    end

    def access_token
      token = Doorkeeper.config.access_token_model.by_token(params["token"])
      return unless token

      RevocableTokens::RevocableAccessToken.new(token)
    end

    def strategy
      @strategy ||= server.token_request(params[:grant_type])
    end

    def authorize_response
      @authorize_response ||= begin
        before_successful_authorization
        auth = strategy.authorize
        context = build_context(auth: auth)
        after_successful_authorization(context) unless auth.is_a?(Doorkeeper::OAuth::ErrorResponse)
        auth
      end
    end

    def build_context(**attributes)
      Doorkeeper::OAuth::Hooks::Context.new(**attributes)
    end

    def before_successful_authorization(context = nil)
      Doorkeeper.config.before_successful_authorization.call(self, context)
    end

    def after_successful_authorization(context)
      Doorkeeper.config.after_successful_authorization.call(self, context)
    end

    def revocation_error_response
      error_description = I18n.t(:unauthorized, scope: %i[doorkeeper errors messages revoke])

      { error: :unauthorized_client, error_description: error_description }
    end
  end
end
