Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support token revocation and introspection #1473

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

hafezdivandari
Copy link
Contributor

@hafezdivandari hafezdivandari commented Feb 14, 2025

Fixes #806
Fixes #1350
Closes #1255
Closes #1135
Closes #995
Closes #925

It's much easier to implement Token Revocation RFC7009 and Token Introspection RFC7662 at the same time! These 2 RFCs have most of the logic in common:

  • The same HTTP POST request with the same parameters sent as
    "application/x-www-form-urlencoded" data:
    • token is required, accepts either an access token or a refresh token.
    • token_type_hint is optional to help the server optimize the token lookup. access_token or refresh_token.
  • The same client authentication:
    • Via Authorization: Basic {client_credentials} header
    • Or via client_id and client_secret parameters.
  • The same access/refresh token validation:
    • The token was issued to the client making the request,
    • and is valid/active (has valid signature/encryption, is not expired, and is not revoked).
  • The same HTTP status responses:
    • 400: invalid_request the required token parameter was not sent.
    • 401: invalid_client the request is not authorized. Client credentials were not sent or invalid.
    • 200: The token is revoked, expired, doesn't exists, or was not issued to the client making the request:
      • "Token revocation": The response body is empty.
      • "Token Introspection": The JSON response has the active field set to false. { "active": false }
    • 200: The token is valid, active, and was issued to the client making the request:
      • "Token revocation": The response body is empty.
      • "Token Introspection": The JSON response has the active field set to true. { "active": true, ... }

Implementation

  • Fully backward-compatible (No BC breaking changes at all!).
  • Fully consistent with the current code style and project structure.
  • Uses the existing code within the package without adding any redundancy.
  • Fully customizable. Defines interfaces and allow injecting customized instances.
  • Fully tested.

Summary

Class Hierarchy

Summary of file changes in order of appearance.

New AbstractHandler class

  • Extended by AbstractGrant and AbstractTokenHandler classes.
  • clientRepository, accessTokenRepository, and refreshTokenRepository properties have been moved to this class from child AbstractGrant class.
  • setClientRepository, setAccessTokenRepository, setRefreshTokenRepository, getClientCredentials, parseParam, getRequestParameter, getBasicAuthCredentials, getQueryStringParameter, getCookieParameter, getServerParameter, and validateEncryptedRefreshToken methods have been moved to this class from child AbstractGrant class without any change.
  • validateClient method from child AbstractGrant has been moved to this class with a minor change: The call to getIdentifier() is GrantTypeInterface specific.
  • getClientEntityOrFail method from child AbstractGrant has been moved to this class. This method is also overridden on child AbstractGrant: Call to supportsGrantType method is AbstracGrant specific.
  • New validateEncryptedRefreshToken is extracted from RefreshTokenGrant::validateOldRefreshToken method.

Change AuthorizationValidators\BearerTokenValidator class

  • Now implements new JwtValidatorInterface interface.
  • Extract the JWT validation logic from validateAuthorization method into a new validateJwt method.
  • The new validateJwt method also accepts optional $clientId argument. If a $clientId is provided, it verifies whether the token was issued to the given client.

New AuthorizationValidators\JwtValidatorInterface interface

  • Implemented by BearerTokenValidator class.
  • Defines validateJwt method.
  • Used by AbstractTokenHandler::setJwtValidator() method.

Change Exception\OAuthServerException class

  • Define new unsupportedTokenType method.

Change Grant\AbstractGrant class

  • Now extends new AbstractHandler class.
  • All removed liens are moved to the parent AbstractHandler class.
  • Overrides getClientEntityOrFail method.

Change Grant\RefreshTokenGrant class

  • The encrypted refresh token validation logic from validateOldRefreshToken method has been extracted into a new parent AbstractHandler::validateEncryptedRefreshToken method.

New Handlers\AbstractTokenHandler class

  • Extends new AbstractHandler class.
  • Implements new TokenHandlerInterface interface.
  • Defines setPublicKey and setJwtValidator methods that to allow injecting a customized JWT validator.
  • Defines validateToken, validateAccessToken and validateRefreshToken methods that have been used by TokenIntrospectionHandler and TokenRevocationHandler classes.

New Handlers\TokenHandlerInterface interface

  • Implemented by new AbstractTokenHandler class.
  • Used by setTokenRevocationHandler and setTokenIntrospectionHandler methods of the TokenServer class.

New Handlers\TokenIntrospectionHandler class

  • Extends new AbstractTokenHandler class.
  • Defines respondToRequest method that responds to "Token Introspection RFC7662" request.
  • Defines setResponseType method that allows injecting a customized response type.

New Handlers\TokenRevocationHandler class

  • Extends new AbstractTokenHandler class.
  • Defines respondToRequest method that responds to "Token Revocation RFC7009" request.

New ResponseTypes\IntrospectionResponse class

  • Implements new IntrospectionResponseTypeInterface interface.
  • Defines generateHttpResponse method to generate "Token Revocation RFC7009" response.

New ResponseTypes\IntrospectionResponseTypeInterface class

  • Implemented by new IntrospectionResponse class.
  • Used by TokenIntrospectionHandler::setResponseType method that allows injecting a customized response type.

New TokenServer class

  • Defines respondToTokenRevocationRequest and respondToTokenIntrospectionRequest methods to respond to the respective request.
  • Defines setTokenRevocationHandler and setTokenIntrospectionHandler methods that allows injecting customized handlers.

@ezequidias
Copy link

@Sephster , as mentioned #1453 (comment), it would be great to have more maintainers for this repository! @hafezdivandari is an excellent professional with extensive knowledge of OAuth2. Having more people to support the project would certainly help its growth and maintenance! 🚀

@hafezdivandari hafezdivandari marked this pull request as ready for review February 16, 2025 17:49
Comment on lines +27 to +34
use EmitterAwarePolyfill;
use CryptTrait;

protected ClientRepositoryInterface $clientRepository;

protected AccessTokenRepositoryInterface $accessTokenRepository;

protected RefreshTokenRepositoryInterface $refreshTokenRepository;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From AbstractGrant class.

Comment on lines +36 to +49
public function setClientRepository(ClientRepositoryInterface $clientRepository): void
{
$this->clientRepository = $clientRepository;
}

public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void
{
$this->accessTokenRepository = $accessTokenRepository;
}

public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void
{
$this->refreshTokenRepository = $refreshTokenRepository;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From AbstractGrant class.

Comment on lines +51 to +81
/**
* Validate the client.
*
* @throws OAuthServerException
*/
protected function validateClient(ServerRequestInterface $request): ClientEntityInterface
{
[$clientId, $clientSecret] = $this->getClientCredentials($request);

$client = $this->getClientEntityOrFail($clientId, $request);

if ($client->isConfidential()) {
if ($clientSecret === '') {
throw OAuthServerException::invalidRequest('client_secret');
}

if (
$this->clientRepository->validateClient(
$clientId,
$clientSecret,
$this instanceof GrantTypeInterface ? $this->getIdentifier() : null
) === false
) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));

throw OAuthServerException::invalidClient($request);
}
}

return $client;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from AbstractGrant class.

$this->clientRepository->validateClient(
$clientId,
$clientSecret,
$this instanceof GrantTypeInterface ? $this->getIdentifier() : null
Copy link
Contributor Author

@hafezdivandari hafezdivandari Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is the only change on this method, since getIdentifier() method is defined on GrantTypeInterface only.

Comment on lines +83 to +105
/**
* Wrapper around ClientRepository::getClientEntity() that ensures we emit
* an event and throw an exception if the repo doesn't return a client
* entity.
*
* This is a bit of defensive coding because the interface contract
* doesn't actually enforce non-null returns/exception-on-no-client so
* getClientEntity might return null. By contrast, this method will
* always either return a ClientEntityInterface or throw.
*
* @throws OAuthServerException
*/
protected function getClientEntityOrFail(string $clientId, ServerRequestInterface $request): ClientEntityInterface
{
$client = $this->clientRepository->getClientEntity($clientId);

if ($client instanceof ClientEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidClient($request);
}

return $client;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from AbstractGrant class.

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class TokenIntrospectionHandler extends AbstractTokenHandler
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class implements "Token Introspection RFC7662". This class also allows customizing the introspection response by injecting a IntrospectionResponseTypeInterface instance.

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class TokenRevocationHandler extends AbstractTokenHandler
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class implements "Token Revocation RFC7009".

use DateTimeInterface;
use Psr\Http\Message\ResponseInterface;

class IntrospectionResponse implements IntrospectionResponseTypeInterface
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class implements Introspection Response. RFC7662 section 2.2


use Psr\Http\Message\ResponseInterface;

interface IntrospectionResponseTypeInterface
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defines an interface for introspection response type.

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class TokenServer implements EmitterAwareInterface
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class defines 2 methods to respond to token revocation and token introspection requests. Also allows injecting customized token revocation/introspection handlers.

This class is inspired by AuthorizationServer and ResourceServer classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Token Revocation question Access token introspection RFC7662
2 participants