applicake/doorkeeper

View on GitHub
lib/doorkeeper/oauth/token_introspection.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

module Doorkeeper
  module OAuth
    # RFC7662 OAuth 2.0 Token Introspection
    #
    # @see https://tools.ietf.org/html/rfc7662
    class TokenIntrospection
      attr_reader :server, :token
      attr_reader :error

      def initialize(server, token)
        @server = server
        @token = token

        authorize!
      end

      def authorized?
        @error.blank?
      end

      def to_json
        active? ? success_response : failure_response
      end

      private

      # If the protected resource uses OAuth 2.0 client credentials to
      # authenticate to the introspection endpoint and its credentials are
      # invalid, the authorization server responds with an HTTP 401
      # (Unauthorized) as described in Section 5.2 of OAuth 2.0 [RFC6749].
      #
      # Endpoint must first validate the authentication.
      # If the authentication is invalid, the endpoint should respond with
      # an HTTP 401 status code and an invalid_client response.
      #
      # @see https://www.oauth.com/oauth2-servers/token-introspection-endpoint/
      #
      def authorize!
        # Requested client authorization
        if server.credentials
          @error = :invalid_client unless authorized_client
        else
          # Requested bearer token authorization
          @error = :invalid_request unless authorized_token
        end
      end

      # Client Authentication
      def authorized_client
        @authorized_client ||= server.credentials && server.client
      end

      # Bearer Token Authentication
      def authorized_token
        @authorized_token ||=
          OAuth::Token.authenticate(server.context.request, :from_bearer_authorization)
      end

      # 2.2. Introspection Response
      def success_response
        customize_response(
          active: true,
          scope: @token.scopes_string,
          client_id: @token.try(:application).try(:uid),
          token_type: @token.token_type,
          exp: @token.expires_at.to_i,
          iat: @token.created_at.to_i
        )
      end

      # If the introspection call is properly authorized but the token is not
      # active, does not exist on this server, or the protected resource is
      # not allowed to introspect this particular token, then the
      # authorization server MUST return an introspection response with the
      # "active" field set to "false".  Note that to avoid disclosing too
      # much of the authorization server's state to a third party, the
      # authorization server SHOULD NOT include any additional information
      # about an inactive token, including why the token is inactive.
      #
      # @see https://tools.ietf.org/html/rfc7662 2.2. Introspection Response
      #
      def failure_response
        {
          active: false
        }
      end

      # Boolean indicator of whether or not the presented token
      # is currently active.  The specifics of a token's "active" state
      # will vary depending on the implementation of the authorization
      # server and the information it keeps about its tokens, but a "true"
      # value return for the "active" property will generally indicate
      # that a given token has been issued by this authorization server,
      # has not been revoked by the resource owner, and is within its
      # given time window of validity (e.g., after its issuance time and
      # before its expiration time).
      #
      # Any other error is considered an "inactive" token.
      #
      # * The token requested does not exist or is invalid
      # * The token expired
      # * The token was issued to a different client than is making this request
      #
      def active?
        if authorized_client
          valid_token? && authorized_for_client?
        else
          valid_token?
        end
      end

      # Token can be valid only if it is not expired or revoked.
      def valid_token?
        @token.present? && @token.accessible?
      end

      # If token doesn't belong to some client, then it is public.
      # Otherwise in it required for token to be connected to the same client.
      def authorized_for_client?
        if @token.application.present?
          @token.application == authorized_client.application
        else
          true
        end
      end

      # Allows to customize introspection response.
      # Provides context (controller) and token for generating developer-specific
      # response.
      #
      # @see https://tools.ietf.org/html/rfc7662#section-2.2
      #
      def customize_response(response)
        customized_response = Doorkeeper.configuration.custom_introspection_response.call(
          token,
          server.context
        )
        return response if customized_response.blank?

        response.merge(customized_response)
      end
    end
  end
end