From fbc0289c237e3f1ae111fcaa01bd5509f8377c18 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 14 Feb 2025 23:46:23 +0330 Subject: [PATCH 1/8] support token revocation and introspection --- src/AbstractHandler.php | 281 ++++++++++++++++++ .../BearerTokenValidator.php | 33 +- .../JwtValidatorInterface.php | 17 ++ src/Exception/OAuthServerException.php | 14 + src/Grant/AbstractGrant.php | 218 +------------- src/Grant/RefreshTokenGrant.php | 26 +- src/Handlers/AbstractTokenHandler.php | 104 +++++++ src/Handlers/TokenHandlerInterface.php | 29 ++ src/Handlers/TokenIntrospectionHandler.php | 43 +++ src/Handlers/TokenRevocationHandler.php | 31 ++ src/ResponseTypes/IntrospectionResponse.php | 94 ++++++ .../IntrospectionResponseTypeInterface.php | 21 ++ src/TokenServer.php | 94 ++++++ 13 files changed, 759 insertions(+), 246 deletions(-) create mode 100644 src/AbstractHandler.php create mode 100644 src/AuthorizationValidators/JwtValidatorInterface.php create mode 100644 src/Handlers/AbstractTokenHandler.php create mode 100644 src/Handlers/TokenHandlerInterface.php create mode 100644 src/Handlers/TokenIntrospectionHandler.php create mode 100644 src/Handlers/TokenRevocationHandler.php create mode 100644 src/ResponseTypes/IntrospectionResponse.php create mode 100644 src/ResponseTypes/IntrospectionResponseTypeInterface.php create mode 100644 src/TokenServer.php diff --git a/src/AbstractHandler.php b/src/AbstractHandler.php new file mode 100644 index 000000000..0668b1f7c --- /dev/null +++ b/src/AbstractHandler.php @@ -0,0 +1,281 @@ +clientRepository = $clientRepository; + } + + public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void + { + $this->accessTokenRepository = $accessTokenRepository; + } + + public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void + { + $this->refreshTokenRepository = $refreshTokenRepository; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * Gets the client credentials from the request from the request body or + * the Http Basic Authorization header + * + * @return array{0:non-empty-string,1:string} + * + * @throws OAuthServerException + */ + protected function getClientCredentials(ServerRequestInterface $request): array + { + [$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request); + + $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser); + + if ($clientId === null) { + throw OAuthServerException::invalidRequest('client_id'); + } + + $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword); + + return [$clientId, $clientSecret ?? '']; + } + /** + * Parse request parameter. + * + * @param array $request + * + * @return non-empty-string|null + * + * @throws OAuthServerException + */ + private static function parseParam(string $parameter, array $request, ?string $default = null): ?string + { + $value = $request[$parameter] ?? ''; + + if (is_scalar($value)) { + $value = trim((string) $value); + } else { + throw OAuthServerException::invalidRequest($parameter); + } + + if ($value === '') { + $value = $default === null ? null : trim($default); + + if ($value === '') { + $value = null; + } + } + + return $value; + } + + /** + * Retrieve request parameter. + * + * @return non-empty-string|null + * + * @throws OAuthServerException + */ + protected function getRequestParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string + { + return self::parseParam($parameter, (array) $request->getParsedBody(), $default); + } + + /** + * Retrieve HTTP Basic Auth credentials with the Authorization header + * of a request. First index of the returned array is the username, + * second is the password (so list() will work). If the header does + * not exist, or is otherwise an invalid HTTP Basic header, return + * [null, null]. + * + * @return array{0:non-empty-string,1:string}|array{0:null,1:null} + */ + protected function getBasicAuthCredentials(ServerRequestInterface $request): array + { + if (!$request->hasHeader('Authorization')) { + return [null, null]; + } + + $header = $request->getHeader('Authorization')[0]; + if (stripos($header, 'Basic ') !== 0) { + return [null, null]; + } + + $decoded = base64_decode(substr($header, 6), true); + + if ($decoded === false) { + return [null, null]; + } + + if (str_contains($decoded, ':') === false) { + return [null, null]; // HTTP Basic header without colon isn't valid + } + + [$username, $password] = explode(':', $decoded, 2); + + if ($username === '') { + return [null, null]; + } + + return [$username, $password]; + } + + /** + * Retrieve query string parameter. + * + * @return non-empty-string|null + * + * @throws OAuthServerException + */ + protected function getQueryStringParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string + { + return self::parseParam($parameter, $request->getQueryParams(), $default); + } + + /** + * Retrieve cookie parameter. + * + * @return non-empty-string|null + * + * @throws OAuthServerException + */ + protected function getCookieParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string + { + return self::parseParam($parameter, $request->getCookieParams(), $default); + } + + /** + * Retrieve server parameter. + * + * @return non-empty-string|null + * + * @throws OAuthServerException + */ + protected function getServerParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string + { + return self::parseParam($parameter, $request->getServerParams(), $default); + } + + /** + * Validate the given encrypted refresh token. + * + * @throws OAuthServerException + * + * @return array + */ + protected function validateEncryptedRefreshToken( + ServerRequestInterface $request, + string $encryptedRefreshToken, + string $clientId + ): array { + try { + $refreshToken = $this->decrypt($encryptedRefreshToken); + } catch (Exception $e) { + throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e); + } + + $refreshTokenData = json_decode($refreshToken, true); + + if ($refreshTokenData['client_id'] !== $clientId) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); + throw OAuthServerException::invalidRefreshToken('Token is not linked to client'); + } + + if ($refreshTokenData['expire_time'] < time()) { + throw OAuthServerException::invalidRefreshToken('Token has expired'); + } + + if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { + throw OAuthServerException::invalidRefreshToken('Token has been revoked'); + } + + return $refreshTokenData; + } +} diff --git a/src/AuthorizationValidators/BearerTokenValidator.php b/src/AuthorizationValidators/BearerTokenValidator.php index 0442dd48e..f03254a2e 100644 --- a/src/AuthorizationValidators/BearerTokenValidator.php +++ b/src/AuthorizationValidators/BearerTokenValidator.php @@ -34,7 +34,7 @@ use function preg_replace; use function trim; -class BearerTokenValidator implements AuthorizationValidatorInterface +class BearerTokenValidator implements AuthorizationValidatorInterface, JwtValidatorInterface { use CryptTrait; @@ -99,6 +99,21 @@ public function validateAuthorization(ServerRequestInterface $request): ServerRe throw OAuthServerException::accessDenied('Missing "Bearer" token'); } + $claims = $this->validateJwt($request, $jwt); + + // Return the request with additional attributes + return $request + ->withAttribute('oauth_access_token_id', $claims['jti'] ?? null) + ->withAttribute('oauth_client_id', $claims['aud'][0] ?? null) + ->withAttribute('oauth_user_id', $claims['sub'] ?? null) + ->withAttribute('oauth_scopes', $claims['scopes'] ?? null); + } + + /** + * {@inheritdoc} + */ + public function validateJwt(ServerRequestInterface $request, string $jwt, ?string $clientId = null): array + { try { // Attempt to parse the JWT $token = $this->jwtConfiguration->parser()->parse($jwt); @@ -120,16 +135,20 @@ public function validateAuthorization(ServerRequestInterface $request): ServerRe $claims = $token->claims(); + // Check if token is linked to the client + if ( + $clientId !== null && + $claims->get('client_id') !== $clientId && + !$token->isPermittedFor($clientId) + ) { + throw OAuthServerException::accessDenied('Access token is not linked to client'); + } + // Check if token has been revoked if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) { throw OAuthServerException::accessDenied('Access token has been revoked'); } - // Return the request with additional attributes - return $request - ->withAttribute('oauth_access_token_id', $claims->get('jti')) - ->withAttribute('oauth_client_id', $claims->get('aud')[0]) - ->withAttribute('oauth_user_id', $claims->get('sub')) - ->withAttribute('oauth_scopes', $claims->get('scopes')); + return $claims->all(); } } diff --git a/src/AuthorizationValidators/JwtValidatorInterface.php b/src/AuthorizationValidators/JwtValidatorInterface.php new file mode 100644 index 000000000..e38b012e7 --- /dev/null +++ b/src/AuthorizationValidators/JwtValidatorInterface.php @@ -0,0 +1,17 @@ + + */ + public function validateJwt(ServerRequestInterface $request, string $jwt, ?string $clientId = null): array; +} diff --git a/src/Exception/OAuthServerException.php b/src/Exception/OAuthServerException.php index df8b58a3d..896b0fd89 100644 --- a/src/Exception/OAuthServerException.php +++ b/src/Exception/OAuthServerException.php @@ -265,6 +265,20 @@ public static function unauthorizedClient(?string $hint = null): static ); } + /** + * Unsupported Token Type error. + */ + public static function unsupportedTokenType(?string $hint = null): static + { + return new static( + 'The authorization server does not support the revocation of the presented token type.', + 15, + 'unsupported_token_type', + 400, + $hint + ); + } + /** * Generate a HTTP response. */ diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index 7c27e95c5..0ec1c1044 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -19,21 +19,17 @@ use DomainException; use Error; use Exception; +use League\OAuth2\Server\AbstractHandler; use League\OAuth2\Server\CryptKeyInterface; -use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; -use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator; -use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; -use League\OAuth2\Server\Repositories\ClientRepositoryInterface; -use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\RequestEvent; @@ -46,36 +42,25 @@ use function array_filter; use function array_key_exists; -use function base64_decode; use function bin2hex; use function explode; use function is_string; use function random_bytes; -use function substr; use function trim; /** * Abstract grant class. */ -abstract class AbstractGrant implements GrantTypeInterface +abstract class AbstractGrant extends AbstractHandler implements GrantTypeInterface { - use EmitterAwarePolyfill; - use CryptTrait; - protected const SCOPE_DELIMITER_STRING = ' '; protected const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 10; - protected ClientRepositoryInterface $clientRepository; - - protected AccessTokenRepositoryInterface $accessTokenRepository; - protected ScopeRepositoryInterface $scopeRepository; protected AuthCodeRepositoryInterface $authCodeRepository; - protected RefreshTokenRepositoryInterface $refreshTokenRepository; - protected UserRepositoryInterface $userRepository; protected DateInterval $refreshTokenTTL; @@ -86,26 +71,11 @@ abstract class AbstractGrant implements GrantTypeInterface protected bool $revokeRefreshTokens = true; - public function setClientRepository(ClientRepositoryInterface $clientRepository): void - { - $this->clientRepository = $clientRepository; - } - - public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository): void - { - $this->accessTokenRepository = $accessTokenRepository; - } - public function setScopeRepository(ScopeRepositoryInterface $scopeRepository): void { $this->scopeRepository = $scopeRepository; } - public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository): void - { - $this->refreshTokenRepository = $refreshTokenRepository; - } - public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository): void { $this->authCodeRepository = $authCodeRepository; @@ -143,51 +113,11 @@ public function revokeRefreshTokens(bool $willRevoke): void } /** - * 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->getIdentifier()) === false) { - $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); - - throw OAuthServerException::invalidClient($request); - } - } - - return $client; - } - - /** - * 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 + * {@inheritdoc} */ 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); - } + $client = parent::getClientEntityOrFail($clientId, $request); if ($this->supportsGrantType($client, $this->getIdentifier()) === false) { throw OAuthServerException::unauthorizedClient(); @@ -205,29 +135,6 @@ protected function supportsGrantType(ClientEntityInterface $client, string $gran || $client->supportsGrantType($grantType) === true; } - /** - * Gets the client credentials from the request from the request body or - * the Http Basic Authorization header - * - * @return array{0:non-empty-string,1:string} - * - * @throws OAuthServerException - */ - protected function getClientCredentials(ServerRequestInterface $request): array - { - [$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request); - - $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser); - - if ($clientId === null) { - throw OAuthServerException::invalidRequest('client_id'); - } - - $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword); - - return [$clientId, $clientSecret ?? '']; - } - /** * Validate redirectUri from the request. If a redirect URI is provided * ensure it matches what is pre-registered @@ -289,123 +196,6 @@ private function convertScopesQueryStringToArray(string $scopes): array return array_filter(explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), static fn ($scope) => $scope !== ''); } - /** - * Parse request parameter. - * - * @param array $request - * - * @return non-empty-string|null - * - * @throws OAuthServerException - */ - private static function parseParam(string $parameter, array $request, ?string $default = null): ?string - { - $value = $request[$parameter] ?? ''; - - if (is_scalar($value)) { - $value = trim((string) $value); - } else { - throw OAuthServerException::invalidRequest($parameter); - } - - if ($value === '') { - $value = $default === null ? null : trim($default); - - if ($value === '') { - $value = null; - } - } - - return $value; - } - - /** - * Retrieve request parameter. - * - * @return non-empty-string|null - * - * @throws OAuthServerException - */ - protected function getRequestParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string - { - return self::parseParam($parameter, (array) $request->getParsedBody(), $default); - } - - /** - * Retrieve HTTP Basic Auth credentials with the Authorization header - * of a request. First index of the returned array is the username, - * second is the password (so list() will work). If the header does - * not exist, or is otherwise an invalid HTTP Basic header, return - * [null, null]. - * - * @return array{0:non-empty-string,1:string}|array{0:null,1:null} - */ - protected function getBasicAuthCredentials(ServerRequestInterface $request): array - { - if (!$request->hasHeader('Authorization')) { - return [null, null]; - } - - $header = $request->getHeader('Authorization')[0]; - if (stripos($header, 'Basic ') !== 0) { - return [null, null]; - } - - $decoded = base64_decode(substr($header, 6), true); - - if ($decoded === false) { - return [null, null]; - } - - if (str_contains($decoded, ':') === false) { - return [null, null]; // HTTP Basic header without colon isn't valid - } - - [$username, $password] = explode(':', $decoded, 2); - - if ($username === '') { - return [null, null]; - } - - return [$username, $password]; - } - - /** - * Retrieve query string parameter. - * - * @return non-empty-string|null - * - * @throws OAuthServerException - */ - protected function getQueryStringParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string - { - return self::parseParam($parameter, $request->getQueryParams(), $default); - } - - /** - * Retrieve cookie parameter. - * - * @return non-empty-string|null - * - * @throws OAuthServerException - */ - protected function getCookieParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string - { - return self::parseParam($parameter, $request->getCookieParams(), $default); - } - - /** - * Retrieve server parameter. - * - * @return non-empty-string|null - * - * @throws OAuthServerException - */ - protected function getServerParameter(string $parameter, ServerRequestInterface $request, ?string $default = null): ?string - { - return self::parseParam($parameter, $request->getServerParams(), $default); - } - /** * Issue an access token. * diff --git a/src/Grant/RefreshTokenGrant.php b/src/Grant/RefreshTokenGrant.php index 34e3f20b4..06a177df3 100644 --- a/src/Grant/RefreshTokenGrant.php +++ b/src/Grant/RefreshTokenGrant.php @@ -15,7 +15,6 @@ namespace League\OAuth2\Server\Grant; use DateInterval; -use Exception; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; @@ -26,8 +25,6 @@ use function implode; use function in_array; -use function json_decode; -use function time; /** * Refresh token grant. @@ -107,28 +104,7 @@ protected function validateOldRefreshToken(ServerRequestInterface $request, stri $encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request) ?? throw OAuthServerException::invalidRequest('refresh_token'); - // Validate refresh token - try { - $refreshToken = $this->decrypt($encryptedRefreshToken); - } catch (Exception $e) { - throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e); - } - - $refreshTokenData = json_decode($refreshToken, true); - if ($refreshTokenData['client_id'] !== $clientId) { - $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); - throw OAuthServerException::invalidRefreshToken('Token is not linked to client'); - } - - if ($refreshTokenData['expire_time'] < time()) { - throw OAuthServerException::invalidRefreshToken('Token has expired'); - } - - if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { - throw OAuthServerException::invalidRefreshToken('Token has been revoked'); - } - - return $refreshTokenData; + return $this->validateEncryptedRefreshToken($request, $encryptedRefreshToken, $clientId); } /** diff --git a/src/Handlers/AbstractTokenHandler.php b/src/Handlers/AbstractTokenHandler.php new file mode 100644 index 000000000..12abf0ec7 --- /dev/null +++ b/src/Handlers/AbstractTokenHandler.php @@ -0,0 +1,104 @@ +publicKey = $publicKey; + } + + public function setJwtValidator(JwtValidatorInterface $jwtValidator): void + { + $this->jwtValidator = $jwtValidator; + } + + protected function getJwtValidator(): JwtValidatorInterface + { + if ($this->jwtValidator instanceof JwtValidatorInterface === false) { + $this->jwtValidator = new BearerTokenValidator($this->accessTokenRepository); + } + + if ($this->jwtValidator instanceof BearerTokenValidator === true) { + $this->jwtValidator->setPublicKey($this->publicKey); + } + + return $this->jwtValidator; + } + + /** + * @return array{0:non-empty-string, 1:array}|array{0:null, 1:null} + * + * @throws OAuthServerException + */ + protected function validateToken( + ServerRequestInterface $request, + ClientEntityInterface $client + ): array { + $token = $this->getRequestParameter('token', $request) + ?? throw OAuthServerException::invalidRequest('token'); + + $tokenTypeHint = $this->getRequestParameter('token_type_hint', $request, 'access_token'); + + if ($tokenTypeHint === 'refresh_token') { + return $this->validateRefreshToken($request, $token, $client) + ?? $this->validateAccessToken($request, $token, $client) + ?? [null, null]; + } + + return $this->validateAccessToken($request, $token, $client) + ?? $this->validateRefreshToken($request, $token, $client) + ?? [null, null]; + } + + /** + * @return array{0:non-empty-string, 1:array}|null + */ + protected function validateRefreshToken( + ServerRequestInterface $request, + string $refreshToken, + ClientEntityInterface $client + ): ?array { + try { + return [ + 'refresh_token', + $this->validateEncryptedRefreshToken($request, $refreshToken, $client->getIdentifier()), + ]; + } catch (OAuthServerException) { + return null; + } + } + + /** + * @return array{0:non-empty-string, 1:array}|null + */ + protected function validateAccessToken( + ServerRequestInterface $request, + string $accessToken, + ClientEntityInterface $client + ): ?array { + try { + return [ + 'access_token', + $this->getJwtValidator()->validateJwt($request, $accessToken, $client->getIdentifier()) + ]; + } catch (OAuthServerException) { + return null; + } + } +} diff --git a/src/Handlers/TokenHandlerInterface.php b/src/Handlers/TokenHandlerInterface.php new file mode 100644 index 000000000..7165eea86 --- /dev/null +++ b/src/Handlers/TokenHandlerInterface.php @@ -0,0 +1,29 @@ +responseType === null ? new IntrospectionResponse() : clone $this->responseType; + } + + public function setResponseType(IntrospectionResponseTypeInterface $responseType): void + { + $this->responseType = $responseType; + } + + public function respondToRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $client = $this->validateClient($request); + [$tokenType, $token] = $this->validateToken($request, $client); + + $responseType = $this->getResponseType(); + + if ($tokenType !== null && $token !== null) { + $responseType->setActive(true); + $responseType->setTokenType($tokenType); + $responseType->setToken($token); + } else { + $responseType->setActive(false); + } + + return $responseType->generateHttpResponse($response); + } +} diff --git a/src/Handlers/TokenRevocationHandler.php b/src/Handlers/TokenRevocationHandler.php new file mode 100644 index 000000000..2480afa87 --- /dev/null +++ b/src/Handlers/TokenRevocationHandler.php @@ -0,0 +1,31 @@ +validateClient($request); + [$tokenType, $token] = $this->validateToken($request, $client); + + if ($tokenType !== null && $token !== null) { + if ($tokenType === 'refresh_token') { + $this->refreshTokenRepository->revokeRefreshToken($token['refresh_token_id']); + $this->accessTokenRepository->revokeAccessToken($token['access_token_id']); + } elseif ($tokenType === 'access_token') { + $this->accessTokenRepository->revokeAccessToken($token['jti']); + } + } + + return $response + ->withStatus(200) + ->withHeader('pragma', 'no-cache') + ->withHeader('cache-control', 'no-store'); + } +} diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php new file mode 100644 index 000000000..084e94dd7 --- /dev/null +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -0,0 +1,94 @@ + + */ + private ?array $token = null; + + public function setActive(bool $active): void + { + $this->active = $active; + } + + public function setTokenType(string $tokenType): void + { + $this->tokenType = $tokenType; + } + + /** + * {@inheritdoc} + */ + public function setToken(array $token): void + { + $this->token = $token; + } + + public function generateHttpResponse(ResponseInterface $response): ResponseInterface + { + $params = [ + 'active' => $this->active, + ]; + + if ($this->active === true && $this->tokenType !== null && $this->token !== null) { + if ($this->tokenType === 'access_token') { + $params = array_merge($params, array_filter([ + 'scope' => $token['scope'] ?? implode(' ', $token['scopes'] ?? []), + 'client_id' => $token['client_id'] ?? $token['aud'][0] ?? null, + 'username' => $token['username'] ?? null, + 'token_type' => 'Bearer', + 'exp' => $token['exp'] ?? null, + 'iat' => $token['iat'] ?? null, + 'nbf' => $token['nbf'] ?? null, + 'sub' => $token['sub'] ?? null, + 'aud' => $token['aud'] ?? null, + 'iss' => $token['iss'] ?? null, + 'jti' => $token['jti'] ?? null, + ])); + } elseif ($this->tokenType === 'refresh_token') { + $params = array_merge($params, array_filter([ + 'scope' => implode(' ', $token['scopes'] ?? []), + 'client_id' => $token['client_id'] ?? null, + 'exp' => $token['expire_time'] ?? null, + 'sub' => $token['user_id'] ?? null, + 'jti' => $token['refresh_token_id'] ?? null, + ])); + } + + $params = array_merge($params, $this->getExtraParams($this->tokenType, $this->token)); + } + + $params = json_encode($params, flags: JSON_THROW_ON_ERROR); + + $response = $response + ->withStatus(200) + ->withHeader('pragma', 'no-cache') + ->withHeader('cache-control', 'no-store') + ->withHeader('content-type', 'application/json; charset=UTF-8'); + + $response->getBody()->write($params); + + return $response; + } + + /** + * @param non-empty-string $tokenType + * @param array $token + * @return array + */ + protected function getExtraParams(string $tokenType, array $token): array + { + return []; + } +} diff --git a/src/ResponseTypes/IntrospectionResponseTypeInterface.php b/src/ResponseTypes/IntrospectionResponseTypeInterface.php new file mode 100644 index 000000000..4a791218e --- /dev/null +++ b/src/ResponseTypes/IntrospectionResponseTypeInterface.php @@ -0,0 +1,21 @@ + $token + */ + public function setToken(array $token): void; + + public function generateHttpResponse(ResponseInterface $response): ResponseInterface; +} diff --git a/src/TokenServer.php b/src/TokenServer.php new file mode 100644 index 000000000..02467dc3d --- /dev/null +++ b/src/TokenServer.php @@ -0,0 +1,94 @@ +publicKey = $publicKey; + } + + public function setTokenRevocationHandler(TokenRevocationHandler $handler): void + { + $this->tokenRevocationHandler = $handler; + } + + public function setTokenIntrospectionHandler(TokenIntrospectionHandler $handler): void + { + $this->tokenIntrospectionHandler = $handler; + } + + protected function getTokenRevocationHandler(): TokenHandlerInterface + { + $this->tokenRevocationHandler ??= new TokenRevocationHandler(); + + $this->tokenRevocationHandler->setClientRepository($this->clientRepository); + $this->tokenRevocationHandler->setAccessTokenRepository($this->accessTokenRepository); + $this->tokenRevocationHandler->setRefreshTokenRepository($this->refreshTokenRepository); + $this->tokenRevocationHandler->setPublicKey($this->publicKey); + $this->tokenRevocationHandler->setEmitter($this->getEmitter()); + $this->tokenRevocationHandler->setEncryptionKey($this->encryptionKey); + + return $this->tokenRevocationHandler; + } + + protected function getTokenIntrospectionHandler(): TokenHandlerInterface + { + $this->tokenIntrospectionHandler ??= new TokenIntrospectionHandler(); + + $this->tokenIntrospectionHandler->setClientRepository($this->clientRepository); + $this->tokenIntrospectionHandler->setAccessTokenRepository($this->accessTokenRepository); + $this->tokenIntrospectionHandler->setRefreshTokenRepository($this->refreshTokenRepository); + $this->tokenIntrospectionHandler->setPublicKey($this->publicKey); + $this->tokenIntrospectionHandler->setEmitter($this->getEmitter()); + $this->tokenIntrospectionHandler->setEncryptionKey($this->encryptionKey); + + return $this->tokenIntrospectionHandler; + } + + public function respondToTokenRevocationRequest( + ServerRequestInterface $request, + ResponseInterface $response + ): ResponseInterface { + return $this->getTokenRevocationHandler()->respondToRequest($request, $response); + } + + public function respondToTokenIntrospectionRequest( + ServerRequestInterface $request, + ResponseInterface $response + ): ResponseInterface { + return $this->getTokenIntrospectionHandler()->respondToRequest($request, $response); + } +} From 704a3cfde254a3fbb2972e88aa0c44588682cecd Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sat, 15 Feb 2025 00:02:05 +0330 Subject: [PATCH 2/8] formatting --- src/AbstractHandler.php | 3 +- .../JwtValidatorInterface.php | 3 ++ src/Handlers/AbstractTokenHandler.php | 4 +- src/ResponseTypes/IntrospectionResponse.php | 39 +++++++++++-------- .../IntrospectionResponseTypeInterface.php | 3 ++ 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/AbstractHandler.php b/src/AbstractHandler.php index 0668b1f7c..8b8bca482 100644 --- a/src/AbstractHandler.php +++ b/src/AbstractHandler.php @@ -126,6 +126,7 @@ protected function getClientCredentials(ServerRequestInterface $request): array return [$clientId, $clientSecret ?? '']; } + /** * Parse request parameter. * @@ -248,7 +249,7 @@ protected function getServerParameter(string $parameter, ServerRequestInterface * * @throws OAuthServerException * - * @return array + * @return array */ protected function validateEncryptedRefreshToken( ServerRequestInterface $request, diff --git a/src/AuthorizationValidators/JwtValidatorInterface.php b/src/AuthorizationValidators/JwtValidatorInterface.php index e38b012e7..e71787b8d 100644 --- a/src/AuthorizationValidators/JwtValidatorInterface.php +++ b/src/AuthorizationValidators/JwtValidatorInterface.php @@ -11,6 +11,9 @@ interface JwtValidatorInterface /** * Parse and validate the given JWT. * + * @param non-empty-string $jwt + * @param non-empty-string|null $clientId + * * @return array */ public function validateJwt(ServerRequestInterface $request, string $jwt, ?string $clientId = null): array; diff --git a/src/Handlers/AbstractTokenHandler.php b/src/Handlers/AbstractTokenHandler.php index 12abf0ec7..04c140c89 100644 --- a/src/Handlers/AbstractTokenHandler.php +++ b/src/Handlers/AbstractTokenHandler.php @@ -85,6 +85,8 @@ protected function validateRefreshToken( } /** + * @param non-empty-string $accessToken + * * @return array{0:non-empty-string, 1:array}|null */ protected function validateAccessToken( @@ -95,7 +97,7 @@ protected function validateAccessToken( try { return [ 'access_token', - $this->getJwtValidator()->validateJwt($request, $accessToken, $client->getIdentifier()) + $this->getJwtValidator()->validateJwt($request, $accessToken, $client->getIdentifier()), ]; } catch (OAuthServerException) { return null; diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 084e94dd7..d02d33023 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -10,6 +10,9 @@ class IntrospectionResponse implements IntrospectionResponseTypeInterface { private bool $active = false; + /** + * @var non-empty-string|null + */ private ?string $tokenType = null; /** @@ -22,6 +25,9 @@ public function setActive(bool $active): void $this->active = $active; } + /** + * {@inheritdoc} + */ public function setTokenType(string $tokenType): void { $this->tokenType = $tokenType; @@ -44,25 +50,25 @@ public function generateHttpResponse(ResponseInterface $response): ResponseInter if ($this->active === true && $this->tokenType !== null && $this->token !== null) { if ($this->tokenType === 'access_token') { $params = array_merge($params, array_filter([ - 'scope' => $token['scope'] ?? implode(' ', $token['scopes'] ?? []), - 'client_id' => $token['client_id'] ?? $token['aud'][0] ?? null, - 'username' => $token['username'] ?? null, + 'scope' => $this->token['scope'] ?? implode(' ', $this->token['scopes'] ?? []), + 'client_id' => $this->token['client_id'] ?? $this->token['aud'][0] ?? null, + 'username' => $this->token['username'] ?? null, 'token_type' => 'Bearer', - 'exp' => $token['exp'] ?? null, - 'iat' => $token['iat'] ?? null, - 'nbf' => $token['nbf'] ?? null, - 'sub' => $token['sub'] ?? null, - 'aud' => $token['aud'] ?? null, - 'iss' => $token['iss'] ?? null, - 'jti' => $token['jti'] ?? null, + 'exp' => $this->token['exp'] ?? null, + 'iat' => $this->token['iat'] ?? null, + 'nbf' => $this->token['nbf'] ?? null, + 'sub' => $this->token['sub'] ?? null, + 'aud' => $this->token['aud'] ?? null, + 'iss' => $this->token['iss'] ?? null, + 'jti' => $this->token['jti'] ?? null, ])); } elseif ($this->tokenType === 'refresh_token') { $params = array_merge($params, array_filter([ - 'scope' => implode(' ', $token['scopes'] ?? []), - 'client_id' => $token['client_id'] ?? null, - 'exp' => $token['expire_time'] ?? null, - 'sub' => $token['user_id'] ?? null, - 'jti' => $token['refresh_token_id'] ?? null, + 'scope' => implode(' ', $this->token['scopes'] ?? []), + 'client_id' => $this->token['client_id'] ?? null, + 'exp' => $this->token['expire_time'] ?? null, + 'sub' => $this->token['user_id'] ?? null, + 'jti' => $this->token['refresh_token_id'] ?? null, ])); } @@ -83,8 +89,9 @@ public function generateHttpResponse(ResponseInterface $response): ResponseInter } /** - * @param non-empty-string $tokenType + * @param non-empty-string $tokenType * @param array $token + * * @return array */ protected function getExtraParams(string $tokenType, array $token): array diff --git a/src/ResponseTypes/IntrospectionResponseTypeInterface.php b/src/ResponseTypes/IntrospectionResponseTypeInterface.php index 4a791218e..79839869d 100644 --- a/src/ResponseTypes/IntrospectionResponseTypeInterface.php +++ b/src/ResponseTypes/IntrospectionResponseTypeInterface.php @@ -10,6 +10,9 @@ interface IntrospectionResponseTypeInterface { public function setActive(bool $active): void; + /** + * @param non-empty-string $tokenType + */ public function setTokenType(string $tokenType): void; /** From 3482d6f53e646411930061ff5a5a791f9e5f2355 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 16 Feb 2025 21:10:13 +0330 Subject: [PATCH 3/8] formatting --- src/ResponseTypes/IntrospectionResponse.php | 80 +++++++++++++-------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index d02d33023..75b951a6f 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -4,21 +4,22 @@ namespace League\OAuth2\Server\ResponseTypes; +use DateTimeInterface; use Psr\Http\Message\ResponseInterface; class IntrospectionResponse implements IntrospectionResponseTypeInterface { - private bool $active = false; + protected bool $active = false; /** * @var non-empty-string|null */ - private ?string $tokenType = null; + protected ?string $tokenType = null; /** * @var array */ - private ?array $token = null; + protected ?array $token = null; public function setActive(bool $active): void { @@ -48,31 +49,11 @@ public function generateHttpResponse(ResponseInterface $response): ResponseInter ]; if ($this->active === true && $this->tokenType !== null && $this->token !== null) { - if ($this->tokenType === 'access_token') { - $params = array_merge($params, array_filter([ - 'scope' => $this->token['scope'] ?? implode(' ', $this->token['scopes'] ?? []), - 'client_id' => $this->token['client_id'] ?? $this->token['aud'][0] ?? null, - 'username' => $this->token['username'] ?? null, - 'token_type' => 'Bearer', - 'exp' => $this->token['exp'] ?? null, - 'iat' => $this->token['iat'] ?? null, - 'nbf' => $this->token['nbf'] ?? null, - 'sub' => $this->token['sub'] ?? null, - 'aud' => $this->token['aud'] ?? null, - 'iss' => $this->token['iss'] ?? null, - 'jti' => $this->token['jti'] ?? null, - ])); - } elseif ($this->tokenType === 'refresh_token') { - $params = array_merge($params, array_filter([ - 'scope' => implode(' ', $this->token['scopes'] ?? []), - 'client_id' => $this->token['client_id'] ?? null, - 'exp' => $this->token['expire_time'] ?? null, - 'sub' => $this->token['user_id'] ?? null, - 'jti' => $this->token['refresh_token_id'] ?? null, - ])); - } - - $params = array_merge($params, $this->getExtraParams($this->tokenType, $this->token)); + $params = array_merge( + $params, + $this->parseParams($this->tokenType, $this->token), + $this->getExtraParams($this->tokenType, $this->token) + ); } $params = json_encode($params, flags: JSON_THROW_ON_ERROR); @@ -88,6 +69,49 @@ public function generateHttpResponse(ResponseInterface $response): ResponseInter return $response; } + /** + * @param non-empty-string $tokenType + * @param array $token + * + * @return array + */ + protected function parseParams(string $tokenType, array $token): array + { + if ($tokenType === 'access_token') { + return array_filter([ + 'scope' => $token['scope'] ?? implode(' ', $token['scopes'] ?? []), + 'client_id' => $token['client_id'] ?? $token['aud'][0] ?? null, + 'username' => $token['username'] ?? null, + 'token_type' => 'Bearer', + 'exp' => isset($token['exp']) ? $this->convertTimestamp($token['exp']) : null, + 'iat' => isset($token['iat']) ? $this->convertTimestamp($token['iat']) : null, + 'nbf' => isset($token['nbf']) ? $this->convertTimestamp($token['nbf']) : null, + 'sub' => $token['sub'] ?? null, + 'aud' => $token['aud'] ?? null, + 'iss' => $token['iss'] ?? null, + 'jti' => $token['jti'] ?? null, + ]); + } elseif ($tokenType === 'refresh_token') { + return array_filter([ + 'scope' => implode(' ', $token['scopes'] ?? []), + 'client_id' => $token['client_id'] ?? null, + 'exp' => isset($token['expire_time']) ? $this->convertTimestamp($token['expire_time']) : null, + 'sub' => $token['user_id'] ?? null, + 'jti' => $token['refresh_token_id'] ?? null, + ]); + } else { + return []; + } + } + + protected function convertTimestamp(int|float|string|DateTimeInterface $value): int + { + return match (true) { + $value instanceof DateTimeInterface => $value->getTimestamp(), + default => intval($value), + }; + } + /** * @param non-empty-string $tokenType * @param array $token From 68eded04ae9b2a5178fea9793a87876af556de97 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 18 Feb 2025 03:57:59 +0330 Subject: [PATCH 4/8] formatting --- src/Handlers/AbstractTokenHandler.php | 5 +++-- src/TokenServer.php | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Handlers/AbstractTokenHandler.php b/src/Handlers/AbstractTokenHandler.php index 04c140c89..b0e519692 100644 --- a/src/Handlers/AbstractTokenHandler.php +++ b/src/Handlers/AbstractTokenHandler.php @@ -11,6 +11,7 @@ use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use Psr\Http\Message\ServerRequestInterface; +use Throwable; abstract class AbstractTokenHandler extends AbstractHandler implements TokenHandlerInterface { @@ -79,7 +80,7 @@ protected function validateRefreshToken( 'refresh_token', $this->validateEncryptedRefreshToken($request, $refreshToken, $client->getIdentifier()), ]; - } catch (OAuthServerException) { + } catch (Throwable) { return null; } } @@ -99,7 +100,7 @@ protected function validateAccessToken( 'access_token', $this->getJwtValidator()->validateJwt($request, $accessToken, $client->getIdentifier()), ]; - } catch (OAuthServerException) { + } catch (Throwable) { return null; } } diff --git a/src/TokenServer.php b/src/TokenServer.php index 02467dc3d..8d8bfd404 100644 --- a/src/TokenServer.php +++ b/src/TokenServer.php @@ -40,12 +40,12 @@ public function __construct( $this->publicKey = $publicKey; } - public function setTokenRevocationHandler(TokenRevocationHandler $handler): void + public function setTokenRevocationHandler(TokenHandlerInterface $handler): void { $this->tokenRevocationHandler = $handler; } - public function setTokenIntrospectionHandler(TokenIntrospectionHandler $handler): void + public function setTokenIntrospectionHandler(TokenHandlerInterface $handler): void { $this->tokenIntrospectionHandler = $handler; } From f52f03deff3cc1f6778512c341f86d6cf52a1701 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 18 Feb 2025 03:58:08 +0330 Subject: [PATCH 5/8] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5ed6a76ac..586fec265 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,10 @@ The following RFCs are implemented: * [RFC6749 "OAuth 2.0"](https://tools.ietf.org/html/rfc6749) * [RFC6750 "The OAuth 2.0 Authorization Framework: Bearer Token Usage"](https://tools.ietf.org/html/rfc6750) +* [RFC7009 "OAuth 2.0 Token Revocation"](https://tools.ietf.org/html/rfc7009) * [RFC7519 "JSON Web Token (JWT)"](https://tools.ietf.org/html/rfc7519) * [RFC7636 "Proof Key for Code Exchange by OAuth Public Clients"](https://tools.ietf.org/html/rfc7636) +* [RFC7662 "OAuth 2.0 Token Introspection"](https://tools.ietf.org/html/rfc7662) * [RFC8628 "OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628) This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](https://twitter.com/alexbilbie). From 09d284bd3f824eddf74fb1ad3cf26ca8d5d0eead Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 18 Feb 2025 03:58:29 +0330 Subject: [PATCH 6/8] add tests --- tests/Handlers/AbstractTokenHandlerTest.php | 469 ++++++++++++++++++ .../TokenIntrospectionHandlerTest.php | 186 +++++++ tests/Handlers/TokenRevocationHandlerTest.php | 150 ++++++ .../IntrospectionResponseTest.php | 136 +++++ .../IntrospectionResponseWithParams.php | 18 + tests/TokenServerTest.php | 124 +++++ 6 files changed, 1083 insertions(+) create mode 100644 tests/Handlers/AbstractTokenHandlerTest.php create mode 100644 tests/Handlers/TokenIntrospectionHandlerTest.php create mode 100644 tests/Handlers/TokenRevocationHandlerTest.php create mode 100644 tests/ResponseTypes/IntrospectionResponseTest.php create mode 100644 tests/ResponseTypes/IntrospectionResponseWithParams.php create mode 100644 tests/TokenServerTest.php diff --git a/tests/Handlers/AbstractTokenHandlerTest.php b/tests/Handlers/AbstractTokenHandlerTest.php new file mode 100644 index 000000000..88f0a9415 --- /dev/null +++ b/tests/Handlers/AbstractTokenHandlerTest.php @@ -0,0 +1,469 @@ +setEncryptionKey(base64_encode(random_bytes(36))); + } + + public function testSetJwtValidator(): void + { + $request = new ServerRequest(); + $accessToken = 'abcdef'; + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $jwtValidator = $this->createMock(JwtValidatorInterface::class); + $jwtValidator + ->expects($this->once()) + ->method('validateJwt') + ->with($request, 'abcdef', 'client1') + ->willReturn(['foo' => 'bar']); + + $handler = $this->getAbstractTokenHandler(); + $handler->setJwtValidator($jwtValidator); + + $result = (fn () => $this->validateAccessToken($request, $accessToken, $client))->call($handler); + + self::assertSame(['access_token', ['foo' => 'bar']], $result); + } + + public function testValidateToken(): void + { + $client = new ClientEntity(); + + $request = new ServerRequest(); + try { + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken()); + + self::fail('The expected exception was not thrown'); + } catch (OAuthServerException $e) { + self::assertSame('invalid_request', $e->getErrorType()); + } + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1']); + self::assertSame( + ['access_token', ['foo' => 'bar']], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken(accessTokenArray: ['foo' => 'bar'], refreshTokenArray: ['bar' => 'foo'])) + ); + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1']); + self::assertSame( + ['refresh_token', ['foo' => 'bar']], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken(refreshTokenArray: ['foo' => 'bar'])) + ); + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1']); + self::assertSame( + [null, null], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken()) + ); + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'access_token']); + self::assertSame( + ['access_token', ['foo' => 'bar']], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken(accessTokenArray: ['foo' => 'bar'], refreshTokenArray: ['bar' => 'foo'])) + ); + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'access_token']); + self::assertSame( + ['refresh_token', ['bar' => 'foo']], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken(refreshTokenArray: ['bar' => 'foo'])) + ); + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'access_token']); + self::assertSame( + [null, null], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken()) + ); + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'refresh_token']); + self::assertSame( + ['refresh_token', ['bar' => 'foo']], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken(accessTokenArray: ['foo' => 'bar'], refreshTokenArray: ['bar' => 'foo'])) + ); + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'refresh_token']); + self::assertSame( + ['access_token', ['foo' => 'bar']], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken(accessTokenArray: ['foo' => 'bar'])) + ); + + $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'refresh_token']); + self::assertSame( + [null, null], + (fn () => $this->validateToken($request, $client)) + ->call($this->getAbstractTokenHandlerWithToken()) + ); + } + + public function testValidateAccessToken(): void + { + $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); + $accessTokenRepository + ->expects($this->once()) + ->method('isAccessTokenRevoked') + ->with('access1') + ->willReturn(false); + + $handler = $this->getAbstractTokenHandler(); + $handler->setAccessTokenRepository($accessTokenRepository); + + $request = new ServerRequest(); + $expireTime = time() + 1000; + $accessToken = $this->getJwtToken(fn (Builder $builder) => + $builder->permittedFor('client1') + ->relatedTo('user1') + ->identifiedBy('access1') + ->expiresAt((new DateTimeImmutable())->setTimestamp($expireTime)) + ->withClaim('foo', 'bar')); + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + /** @var array{0:non-empty-string, 1:array} $result */ + $result = (fn () => $this->validateAccessToken($request, $accessToken, $client))->call($handler); + $result[1]['exp'] = $result[1]['exp']->getTimestamp(); + + self::assertSame(['access_token', [ + 'aud' => ['client1'], + 'sub' => 'user1', + 'jti' => 'access1', + 'exp' => $expireTime, + 'foo' => 'bar', + ]], $result); + } + + public function testValidateAccessTokenIsRevoked(): void + { + $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); + $accessTokenRepository + ->expects($this->once()) + ->method('isAccessTokenRevoked') + ->with('access1') + ->willReturn(true); + + $handler = $this->getAbstractTokenHandler(); + $handler->setAccessTokenRepository($accessTokenRepository); + + $request = new ServerRequest(); + $expireTime = time() + 1000; + $accessToken = $this->getJwtToken(fn (Builder $builder) => + $builder->permittedFor('client1') + ->relatedTo('user1') + ->identifiedBy('access1') + ->expiresAt((new DateTimeImmutable())->setTimestamp($expireTime))); + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateAccessToken($request, $accessToken, $client))->call($handler); + + self::assertNull($result); + } + + public function testValidateAccessTokenIsExpired(): void + { + $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); + $accessTokenRepository->expects($this->never())->method('isAccessTokenRevoked'); + + $handler = $this->getAbstractTokenHandler(); + $handler->setAccessTokenRepository($accessTokenRepository); + + $request = new ServerRequest(); + $expireTime = time() - 1000; + $accessToken = $this->getJwtToken(fn (Builder $builder) => + $builder->permittedFor('client1') + ->relatedTo('user1') + ->identifiedBy('access1') + ->expiresAt((new DateTimeImmutable())->setTimestamp($expireTime))); + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateAccessToken($request, $accessToken, $client))->call($handler); + + self::assertNull($result); + } + + public function testValidateAccessTokenWithMismatchClient(): void + { + $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); + $accessTokenRepository->expects($this->never())->method('isAccessTokenRevoked'); + + $handler = $this->getAbstractTokenHandler(); + $handler->setAccessTokenRepository($accessTokenRepository); + + $request = new ServerRequest(); + $expireTime = time() + 1000; + $accessToken = $this->getJwtToken(fn (Builder $builder) => + $builder->permittedFor('client2') + ->relatedTo('user1') + ->identifiedBy('access1') + ->expiresAt((new DateTimeImmutable())->setTimestamp($expireTime))); + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateAccessToken($request, $accessToken, $client))->call($handler); + + self::assertNull($result); + } + + public function testValidateAccessTokenWithInvalidToken(): void + { + $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); + $accessTokenRepository->expects($this->never())->method('isAccessTokenRevoked'); + + $handler = $this->getAbstractTokenHandler(); + $handler->setAccessTokenRepository($accessTokenRepository); + + $request = new ServerRequest(); + $accessToken = 'abcdef'; + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateAccessToken($request, $accessToken, $client))->call($handler); + + self::assertNull($result); + } + + public function testValidateRefreshToken(): void + { + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository + ->expects($this->once()) + ->method('isRefreshTokenRevoked') + ->with('refresh1') + ->willReturn(false); + + $handler = $this->getAbstractTokenHandler(); + $handler->setRefreshTokenRepository($refreshTokenRepository); + + $request = new ServerRequest(); + $refreshToken = $this->encrypt(json_encode([ + 'refresh_token_id' => 'refresh1', + 'expire_time' => $expireTime = time() + 1000, + 'client_id' => 'client1', + 'foo' => 'bar', + ], flags: JSON_THROW_ON_ERROR)); + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateRefreshToken($request, $refreshToken, $client))->call($handler); + + self::assertSame(['refresh_token', [ + 'refresh_token_id' => 'refresh1', + 'expire_time' => $expireTime, + 'client_id' => 'client1', + 'foo' => 'bar', + ]], $result); + } + + public function testValidateRefreshTokenIsRevoked(): void + { + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository + ->expects($this->once()) + ->method('isRefreshTokenRevoked') + ->with('refresh1') + ->willReturn(true); + + $handler = $this->getAbstractTokenHandler(); + $handler->setRefreshTokenRepository($refreshTokenRepository); + + $request = new ServerRequest(); + $refreshToken = $this->encrypt(json_encode([ + 'refresh_token_id' => 'refresh1', + 'expire_time' => time() + 1000, + 'client_id' => 'client1', + ], flags: JSON_THROW_ON_ERROR)); + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateRefreshToken($request, $refreshToken, $client))->call($handler); + + self::assertNull($result); + } + + public function testValidateRefreshTokenIsExpired(): void + { + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->never())->method('isRefreshTokenRevoked'); + + $handler = $this->getAbstractTokenHandler(); + $handler->setRefreshTokenRepository($refreshTokenRepository); + + $request = new ServerRequest(); + $refreshToken = $this->encrypt(json_encode([ + 'refresh_token_id' => 'refresh1', + 'expire_time' => time() - 1000, + 'client_id' => 'client1', + ], flags: JSON_THROW_ON_ERROR)); + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateRefreshToken($request, $refreshToken, $client))->call($handler); + + self::assertNull($result); + } + + public function testValidateRefreshTokenWithMismatchClient(): void + { + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->never())->method('isRefreshTokenRevoked'); + + $handler = $this->getAbstractTokenHandler(); + $handler->setRefreshTokenRepository($refreshTokenRepository); + + $request = new ServerRequest(); + $refreshToken = $this->encrypt(json_encode([ + 'refresh_token_id' => 'refresh1', + 'expire_time' => time() + 1000, + 'client_id' => 'client2', + ], flags: JSON_THROW_ON_ERROR)); + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateRefreshToken($request, $refreshToken, $client))->call($handler); + + self::assertNull($result); + } + + public function testValidateRefreshTokenWithInvalidToken(): void + { + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->never())->method('isRefreshTokenRevoked'); + + $handler = $this->getAbstractTokenHandler(); + $handler->setRefreshTokenRepository($refreshTokenRepository); + + $request = new ServerRequest(); + $refreshToken = 'abcdef'; + $client = new ClientEntity(); + $client->setIdentifier('client1'); + + $result = (fn () => $this->validateRefreshToken($request, $refreshToken, $client))->call($handler); + + self::assertNull($result); + } + + private function getAbstractTokenHandler(): AbstractTokenHandler + { + $handler = new class () extends AbstractTokenHandler { + public function respondToRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + return $response; + } + }; + + $handler->setEncryptionKey($this->encryptionKey); + $handler->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + return $handler; + } + + /** + * @param array|null $accessTokenArray + * @param array|null $refreshTokenArray + */ + private function getAbstractTokenHandlerWithToken( + ?array $accessTokenArray = null, + ?array $refreshTokenArray = null, + ): AbstractTokenHandler { + return new class ($accessTokenArray, $refreshTokenArray) extends AbstractTokenHandler { + /** + * @param array|null $accessTokenArray + * @param array|null $refreshTokenArray + */ + public function __construct( + private ?array $accessTokenArray = null, + private ?array $refreshTokenArray = null + ) { + } + + public function respondToRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + return $response; + } + + /** + * {@inheritdoc} + */ + protected function validateAccessToken(ServerRequestInterface $request, string $accessToken, ClientEntityInterface $client): ?array + { + return isset($this->accessTokenArray) ? ['access_token', [...$this->accessTokenArray]] : null; + } + + /** + * {@inheritdoc} + */ + protected function validateRefreshToken(ServerRequestInterface $request, string $refreshToken, ClientEntityInterface $client): ?array + { + return isset($this->refreshTokenArray) ? ['refresh_token', [...$this->refreshTokenArray]] : null; + } + }; + } + + /** + * @param Closure(Builder): Builder $withBuilder + * + * @return non-empty-string + */ + private function getJwtToken(Closure $withBuilder): string + { + $privateKey = new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'); + + $contents = $privateKey->getKeyContents(); + + if ($contents === '') { + $contents = 'empty'; + } + + $configuration = Configuration::forAsymmetricSigner( + new Sha256(), + InMemory::plainText($contents, $privateKey->getPassPhrase() ?? ''), + InMemory::plainText('empty', 'empty') + ); + + return $withBuilder($configuration->builder()) + ->getToken($configuration->signer(), $configuration->signingKey()) + ->toString(); + } +} diff --git a/tests/Handlers/TokenIntrospectionHandlerTest.php b/tests/Handlers/TokenIntrospectionHandlerTest.php new file mode 100644 index 000000000..2e19a57cf --- /dev/null +++ b/tests/Handlers/TokenIntrospectionHandlerTest.php @@ -0,0 +1,186 @@ +setConfidential(); + $client->setIdentifier('client1'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once()) + ->method('getClientEntity') + ->with('client1') + ->willReturn($client); + $clientRepository + ->expects($this->once()) + ->method('validateClient') + ->with('client1', 'secret1', null) + ->willReturn(true); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'client1', + 'client_secret' => 'secret1', + 'token' => 'token1', + ]); + + $handler = $this->getMockBuilder(TokenIntrospectionHandler::class)->onlyMethods(['validateAccessToken'])->getMock(); + $handler->setClientRepository($clientRepository); + $handler->expects($this->once()) + ->method('validateAccessToken') + ->with($request, 'token1', $client) + ->willReturn(['access_token', ['jti' => 'access1']]); + + $response = $handler->respondToRequest($request, new Response()); + $response->getBody()->rewind(); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=UTF-8', $response->getHeaderLine('Content-Type')); + self::assertSame([ + 'active' => true, + 'token_type' => 'Bearer', + 'jti' => 'access1' + ], json_decode($response->getBody()->getContents(), true)); + } + + public function testRespondToRequestForRefreshToken(): void + { + $client = new ClientEntity(); + $client->setConfidential(); + $client->setIdentifier('client1'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once()) + ->method('getClientEntity') + ->with('client1') + ->willReturn($client); + $clientRepository + ->expects($this->once()) + ->method('validateClient') + ->with('client1', 'secret1', null) + ->willReturn(true); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'client1', + 'client_secret' => 'secret1', + 'token' => 'token1', + ]); + + $handler = $this->getMockBuilder(TokenIntrospectionHandler::class)->onlyMethods(['validateRefreshToken'])->getMock(); + $handler->setClientRepository($clientRepository); + $handler->expects($this->once()) + ->method('validateRefreshToken') + ->with($request, 'token1', $client) + ->willReturn(['refresh_token', ['refresh_token_id' => 'refresh1']]); + + $response = $handler->respondToRequest($request, new Response()); + $response->getBody()->rewind(); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=UTF-8', $response->getHeaderLine('Content-Type')); + self::assertSame([ + 'active' => true, + 'jti' => 'refresh1', + ], json_decode($response->getBody()->getContents(), true)); + } + + public function testRespondToRequestForInvalidToken(): void + { + $client = new ClientEntity(); + $client->setConfidential(); + $client->setIdentifier('client1'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once()) + ->method('getClientEntity') + ->with('client1') + ->willReturn($client); + $clientRepository + ->expects($this->once()) + ->method('validateClient') + ->with('client1', 'secret1', null) + ->willReturn(true); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'client1', + 'client_secret' => 'secret1', + 'token' => 'token1', + ]); + + $handler = $this->getMockBuilder(TokenIntrospectionHandler::class) + ->onlyMethods(['validateAccessToken', 'validateRefreshToken'])->getMock(); + $handler->setClientRepository($clientRepository); + $handler->expects($this->once()) + ->method('validateAccessToken') + ->with($request, 'token1', $client) + ->willReturn(null); + $handler->expects($this->once()) + ->method('validateRefreshToken') + ->with($request, 'token1', $client) + ->willReturn(null); + + $response = $handler->respondToRequest($request, new Response()); + $response->getBody()->rewind(); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('application/json; charset=UTF-8', $response->getHeaderLine('Content-Type')); + self::assertSame(['active' => false], json_decode($response->getBody()->getContents(), true)); + } + + public function testSetResponseType(): void + { + $client = new ClientEntity(); + $client->setConfidential(); + $client->setIdentifier('client1'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once()) + ->method('getClientEntity') + ->with('client1') + ->willReturn($client); + $clientRepository + ->expects($this->once()) + ->method('validateClient') + ->with('client1', 'secret1', null) + ->willReturn(true); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'client1', + 'client_secret' => 'secret1', + 'token' => 'token1', + ]); + + $response = new Response(); + + $responseType = $this->createMock(IntrospectionResponseTypeInterface::class); + $responseType->expects($this->once())->method('setActive')->with(true); + $responseType->expects($this->once())->method('setTokenType')->with('foo'); + $responseType->expects($this->once())->method('setToken')->with(['bar' => 'baz']); + $responseType->expects($this->once())->method('generateHttpResponse')->with($response)->willReturnArgument(0); + + $handler = $this->getMockBuilder(TokenIntrospectionHandler::class)->onlyMethods(['validateToken'])->getMock(); + $handler->setClientRepository($clientRepository); + $handler->setResponseType($responseType); + $handler->expects($this->once()) + ->method('validateToken') + ->with($request, $client) + ->willReturn(['foo', ['bar' => 'baz']]); + + $result = $handler->respondToRequest($request, $response); + + self::assertSame($response, $result); + } +} diff --git a/tests/Handlers/TokenRevocationHandlerTest.php b/tests/Handlers/TokenRevocationHandlerTest.php new file mode 100644 index 000000000..d3c4d02d1 --- /dev/null +++ b/tests/Handlers/TokenRevocationHandlerTest.php @@ -0,0 +1,150 @@ +setConfidential(); + $client->setIdentifier('client1'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once()) + ->method('getClientEntity') + ->with('client1') + ->willReturn($client); + $clientRepository + ->expects($this->once()) + ->method('validateClient') + ->with('client1', 'secret1', null) + ->willReturn(true); + + $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); + $accessTokenRepository->expects($this->once())->method('revokeAccessToken')->with('access1'); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'client1', + 'client_secret' => 'secret1', + 'token' => 'token1', + ]); + + $handler = $this->getMockBuilder(TokenRevocationHandler::class)->onlyMethods(['validateAccessToken'])->getMock(); + $handler->setClientRepository($clientRepository); + $handler->setAccessTokenRepository($accessTokenRepository); + $handler->expects($this->once()) + ->method('validateAccessToken') + ->with($request, 'token1', $client) + ->willReturn(['access_token', ['jti' => 'access1']]); + + $response = $handler->respondToRequest($request, new Response()); + $response->getBody()->rewind(); + + self::assertSame(200, $response->getStatusCode()); + } + + public function testRespondToRequestForRefreshToken(): void + { + $client = new ClientEntity(); + $client->setConfidential(); + $client->setIdentifier('client1'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once()) + ->method('getClientEntity') + ->with('client1') + ->willReturn($client); + $clientRepository + ->expects($this->once()) + ->method('validateClient') + ->with('client1', 'secret1', null) + ->willReturn(true); + + $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); + $accessTokenRepository->expects($this->once())->method('revokeAccessToken')->with('access1'); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->once())->method('revokeRefreshToken')->with('refresh1'); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'client1', + 'client_secret' => 'secret1', + 'token' => 'token1', + ]); + + $handler = $this->getMockBuilder(TokenRevocationHandler::class)->onlyMethods(['validateRefreshToken'])->getMock(); + $handler->setClientRepository($clientRepository); + $handler->setAccessTokenRepository($accessTokenRepository); + $handler->setRefreshTokenRepository($refreshTokenRepository); + $handler->expects($this->once()) + ->method('validateRefreshToken') + ->with($request, 'token1', $client) + ->willReturn(['refresh_token', ['refresh_token_id' => 'refresh1', 'access_token_id' => 'access1']]); + + $response = $handler->respondToRequest($request, new Response()); + $response->getBody()->rewind(); + + self::assertSame(200, $response->getStatusCode()); + } + + public function testRespondToRequestForInvalidToken(): void + { + $client = new ClientEntity(); + $client->setConfidential(); + $client->setIdentifier('client1'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once()) + ->method('getClientEntity') + ->with('client1') + ->willReturn($client); + $clientRepository + ->expects($this->once()) + ->method('validateClient') + ->with('client1', 'secret1', null) + ->willReturn(true); + + $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); + $accessTokenRepository->expects($this->never())->method('revokeAccessToken'); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->never())->method('revokeRefreshToken'); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'client1', + 'client_secret' => 'secret1', + 'token' => 'token1', + ]); + + $handler = $this->getMockBuilder(TokenRevocationHandler::class) + ->onlyMethods(['validateAccessToken', 'validateRefreshToken'])->getMock(); + $handler->setClientRepository($clientRepository); + $handler->setAccessTokenRepository($accessTokenRepository); + $handler->setRefreshTokenRepository($refreshTokenRepository); + $handler->expects($this->once()) + ->method('validateAccessToken') + ->with($request, 'token1', $client) + ->willReturn(null); + $handler->expects($this->once()) + ->method('validateRefreshToken') + ->with($request, 'token1', $client) + ->willReturn(null); + + $response = $handler->respondToRequest($request, new Response()); + $response->getBody()->rewind(); + + self::assertSame(200, $response->getStatusCode()); + } +} diff --git a/tests/ResponseTypes/IntrospectionResponseTest.php b/tests/ResponseTypes/IntrospectionResponseTest.php new file mode 100644 index 000000000..35bcc225c --- /dev/null +++ b/tests/ResponseTypes/IntrospectionResponseTest.php @@ -0,0 +1,136 @@ +setActive(true); + $responseType->setTokenType('access_token'); + $responseType->setToken([ + 'scopes' => ['scope1', 'scope2'], + 'aud' => ['client1'], + 'username' => 'username1', + 'exp' => (new DateTimeImmutable())->setTimestamp(123456), + 'iat' => 111111, + 'nbf' => '654321', + 'sub' => 'user1', + 'iss' => 'https://example.com', + 'jti' => 'token1', + ]); + + $response = $responseType->generateHttpResponse(new Response()); + $response->getBody()->rewind(); + + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals('no-cache', $response->getHeader('pragma')[0]); + self::assertEquals('no-store', $response->getHeader('cache-control')[0]); + self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + self::assertSame([ + 'active' => true, + 'scope' => 'scope1 scope2', + 'client_id' => 'client1', + 'username' => 'username1', + 'token_type' => 'Bearer', + 'exp' => 123456, + 'iat' => 111111, + 'nbf' => 654321, + 'sub' => 'user1', + 'aud' => ['client1'], + 'iss' => 'https://example.com', + 'jti' => 'token1', + ], json_decode($response->getBody()->getContents(), true)); + } + + public function testGenerateHttpResponseForRefreshToken(): void + { + $responseType = new IntrospectionResponse(); + $responseType->setActive(true); + $responseType->setTokenType('refresh_token'); + $responseType->setToken([ + 'scopes' => ['scope1', 'scope2'], + 'client_id' => 'client1', + 'expire_time' => (new DateTimeImmutable())->setTimestamp(123456), + 'user_id' => 'user1', + 'refresh_token_id' => 'token1', + ]); + + $response = $responseType->generateHttpResponse(new Response()); + $response->getBody()->rewind(); + + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals('no-cache', $response->getHeader('pragma')[0]); + self::assertEquals('no-store', $response->getHeader('cache-control')[0]); + self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + self::assertSame([ + 'active' => true, + 'scope' => 'scope1 scope2', + 'client_id' => 'client1', + 'exp' => 123456, + 'sub' => 'user1', + 'jti' => 'token1', + ], json_decode($response->getBody()->getContents(), true)); + } + + public function testGenerateHttpResponseForInactiveToken(): void + { + $responseType = new IntrospectionResponse(); + $responseType->setActive(false); + $responseType->setTokenType('access_token'); + $responseType->setToken([ + 'scopes' => ['scope1', 'scope2'], + 'client_id' => 'client1', + ]); + + $response = $responseType->generateHttpResponse(new Response()); + $response->getBody()->rewind(); + + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals('no-cache', $response->getHeader('pragma')[0]); + self::assertEquals('no-store', $response->getHeader('cache-control')[0]); + self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + self::assertSame([ + 'active' => false, + ], json_decode($response->getBody()->getContents(), true)); + } + + public function testGenerateHttpResponseWithExtraParams(): void + { + $responseType = new IntrospectionResponseWithParams(); + $responseType->setActive(true); + $responseType->setTokenType('access_token'); + $responseType->setToken([ + 'scopes' => ['scope1', 'scope2'], + 'client_id' => 'client1', + 'jti' => null, + 'extension' => 'extension1' + ]); + + $response = $responseType->generateHttpResponse(new Response()); + $response->getBody()->rewind(); + + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals('no-cache', $response->getHeader('pragma')[0]); + self::assertEquals('no-store', $response->getHeader('cache-control')[0]); + self::assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + self::assertSame([ + 'active' => true, + 'scope' => 'scope1 scope2', + 'client_id' => 'client1', + 'token_type' => 'Bearer', + 'foo' => 'bar', + 'extended' => 'extension1' + ], json_decode($response->getBody()->getContents(), true)); + } +} diff --git a/tests/ResponseTypes/IntrospectionResponseWithParams.php b/tests/ResponseTypes/IntrospectionResponseWithParams.php new file mode 100644 index 000000000..3b20e4d76 --- /dev/null +++ b/tests/ResponseTypes/IntrospectionResponseWithParams.php @@ -0,0 +1,18 @@ + 'bar', 'extended' => $token['extension']]; + } +} diff --git a/tests/TokenServerTest.php b/tests/TokenServerTest.php new file mode 100644 index 000000000..1549be28c --- /dev/null +++ b/tests/TokenServerTest.php @@ -0,0 +1,124 @@ +setIdentifier('foo'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once())->method('getClientEntity') + ->with('foo') + ->willReturn($client); + + $server = $this->getTokenServer($clientRepository); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'token' => 'foobar', + ]); + + $result = $server->respondToTokenRevocationRequest($request, new Response()); + + self::assertSame(200, $result->getStatusCode()); + } + + public function testRespondToTokenIntrospectionRequest(): void + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + + $clientRepository = $this->createMock(ClientRepositoryInterface::class); + $clientRepository->expects($this->once())->method('getClientEntity') + ->with('foo') + ->willReturn($client); + + $server = $this->getTokenServer($clientRepository); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'token' => 'foobar', + ]); + + $result = $server->respondToTokenIntrospectionRequest($request, new Response()); + $result->getBody()->rewind(); + + self::assertSame(200, $result->getStatusCode()); + self::assertSame('application/json; charset=UTF-8', $result->getHeaderLine('Content-Type')); + self::assertSame('{"active":false}', $result->getBody()->getContents()); + } + + public function testSetTokenRevocationHandler(): void + { + $server = $this->getTokenServer(); + + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $revocationHandler = $this->getMockBuilder(TokenHandlerInterface::class)->getMock(); + $revocationHandler->expects($this->once())->method('respondToRequest') + ->with($request, $response) + ->willReturn($response); + + $server->setTokenRevocationHandler($revocationHandler); + + $result = $server->respondToTokenRevocationRequest($request, $response); + + self::assertSame($response, $result); + } + + public function testSetTokenIntrospectionHandler(): void + { + $server = $this->getTokenServer(); + + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $introspectionHandler = $this->getMockBuilder(TokenHandlerInterface::class)->getMock(); + $introspectionHandler->expects($this->once())->method('respondToRequest') + ->with($request, $response) + ->willReturn($response); + + $server->setTokenIntrospectionHandler($introspectionHandler); + + $result = $server->respondToTokenIntrospectionRequest($request, $response); + + self::assertSame($response, $result); + } + + private function getTokenServer( + ?ClientRepositoryInterface $clientRepository = null, + ?AccessTokenRepositoryInterface $accessTokenRepository = null, + ?RefreshTokenRepositoryInterface $refreshTokenRepository = null + ): TokenServer { + return new TokenServer( + $clientRepository ?? $this->createMock(ClientRepositoryInterface::class), + $accessTokenRepository ?? $this->createMock(AccessTokenRepositoryInterface::class), + $refreshTokenRepository ?? $this->createMock(RefreshTokenRepositoryInterface::class), + 'file://' . __DIR__ . '/Stubs/public.key', + base64_encode(random_bytes(36)) + ); + } +} From 70bf83588ca8c97103119dd8d91afb4d5e70b85f Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Tue, 18 Feb 2025 04:02:55 +0330 Subject: [PATCH 7/8] formatting --- tests/Handlers/AbstractTokenHandlerTest.php | 22 ++++++------ .../TokenIntrospectionHandlerTest.php | 36 +++++++++---------- tests/Handlers/TokenRevocationHandlerTest.php | 30 ++++++++-------- .../IntrospectionResponseTest.php | 4 +-- tests/TokenServerTest.php | 8 ++--- 5 files changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/Handlers/AbstractTokenHandlerTest.php b/tests/Handlers/AbstractTokenHandlerTest.php index 88f0a9415..d0f177518 100644 --- a/tests/Handlers/AbstractTokenHandlerTest.php +++ b/tests/Handlers/AbstractTokenHandlerTest.php @@ -46,7 +46,7 @@ public function testSetJwtValidator(): void $jwtValidator = $this->createMock(JwtValidatorInterface::class); $jwtValidator - ->expects($this->once()) + ->expects(self::once()) ->method('validateJwt') ->with($request, 'abcdef', 'client1') ->willReturn(['foo' => 'bar']); @@ -141,7 +141,7 @@ public function testValidateAccessToken(): void { $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); $accessTokenRepository - ->expects($this->once()) + ->expects(self::once()) ->method('isAccessTokenRevoked') ->with('access1') ->willReturn(false); @@ -177,7 +177,7 @@ public function testValidateAccessTokenIsRevoked(): void { $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); $accessTokenRepository - ->expects($this->once()) + ->expects(self::once()) ->method('isAccessTokenRevoked') ->with('access1') ->willReturn(true); @@ -203,7 +203,7 @@ public function testValidateAccessTokenIsRevoked(): void public function testValidateAccessTokenIsExpired(): void { $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); - $accessTokenRepository->expects($this->never())->method('isAccessTokenRevoked'); + $accessTokenRepository->expects(self::never())->method('isAccessTokenRevoked'); $handler = $this->getAbstractTokenHandler(); $handler->setAccessTokenRepository($accessTokenRepository); @@ -226,7 +226,7 @@ public function testValidateAccessTokenIsExpired(): void public function testValidateAccessTokenWithMismatchClient(): void { $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); - $accessTokenRepository->expects($this->never())->method('isAccessTokenRevoked'); + $accessTokenRepository->expects(self::never())->method('isAccessTokenRevoked'); $handler = $this->getAbstractTokenHandler(); $handler->setAccessTokenRepository($accessTokenRepository); @@ -249,7 +249,7 @@ public function testValidateAccessTokenWithMismatchClient(): void public function testValidateAccessTokenWithInvalidToken(): void { $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); - $accessTokenRepository->expects($this->never())->method('isAccessTokenRevoked'); + $accessTokenRepository->expects(self::never())->method('isAccessTokenRevoked'); $handler = $this->getAbstractTokenHandler(); $handler->setAccessTokenRepository($accessTokenRepository); @@ -268,7 +268,7 @@ public function testValidateRefreshToken(): void { $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); $refreshTokenRepository - ->expects($this->once()) + ->expects(self::once()) ->method('isRefreshTokenRevoked') ->with('refresh1') ->willReturn(false); @@ -300,7 +300,7 @@ public function testValidateRefreshTokenIsRevoked(): void { $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); $refreshTokenRepository - ->expects($this->once()) + ->expects(self::once()) ->method('isRefreshTokenRevoked') ->with('refresh1') ->willReturn(true); @@ -325,7 +325,7 @@ public function testValidateRefreshTokenIsRevoked(): void public function testValidateRefreshTokenIsExpired(): void { $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); - $refreshTokenRepository->expects($this->never())->method('isRefreshTokenRevoked'); + $refreshTokenRepository->expects(self::never())->method('isRefreshTokenRevoked'); $handler = $this->getAbstractTokenHandler(); $handler->setRefreshTokenRepository($refreshTokenRepository); @@ -347,7 +347,7 @@ public function testValidateRefreshTokenIsExpired(): void public function testValidateRefreshTokenWithMismatchClient(): void { $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); - $refreshTokenRepository->expects($this->never())->method('isRefreshTokenRevoked'); + $refreshTokenRepository->expects(self::never())->method('isRefreshTokenRevoked'); $handler = $this->getAbstractTokenHandler(); $handler->setRefreshTokenRepository($refreshTokenRepository); @@ -369,7 +369,7 @@ public function testValidateRefreshTokenWithMismatchClient(): void public function testValidateRefreshTokenWithInvalidToken(): void { $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); - $refreshTokenRepository->expects($this->never())->method('isRefreshTokenRevoked'); + $refreshTokenRepository->expects(self::never())->method('isRefreshTokenRevoked'); $handler = $this->getAbstractTokenHandler(); $handler->setRefreshTokenRepository($refreshTokenRepository); diff --git a/tests/Handlers/TokenIntrospectionHandlerTest.php b/tests/Handlers/TokenIntrospectionHandlerTest.php index 2e19a57cf..e0c894da4 100644 --- a/tests/Handlers/TokenIntrospectionHandlerTest.php +++ b/tests/Handlers/TokenIntrospectionHandlerTest.php @@ -21,12 +21,12 @@ public function testRespondToRequestForAccessToken(): void $client->setIdentifier('client1'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once()) + $clientRepository->expects(self::once()) ->method('getClientEntity') ->with('client1') ->willReturn($client); $clientRepository - ->expects($this->once()) + ->expects(self::once()) ->method('validateClient') ->with('client1', 'secret1', null) ->willReturn(true); @@ -39,7 +39,7 @@ public function testRespondToRequestForAccessToken(): void $handler = $this->getMockBuilder(TokenIntrospectionHandler::class)->onlyMethods(['validateAccessToken'])->getMock(); $handler->setClientRepository($clientRepository); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateAccessToken') ->with($request, 'token1', $client) ->willReturn(['access_token', ['jti' => 'access1']]); @@ -52,7 +52,7 @@ public function testRespondToRequestForAccessToken(): void self::assertSame([ 'active' => true, 'token_type' => 'Bearer', - 'jti' => 'access1' + 'jti' => 'access1', ], json_decode($response->getBody()->getContents(), true)); } @@ -63,12 +63,12 @@ public function testRespondToRequestForRefreshToken(): void $client->setIdentifier('client1'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once()) + $clientRepository->expects(self::once()) ->method('getClientEntity') ->with('client1') ->willReturn($client); $clientRepository - ->expects($this->once()) + ->expects(self::once()) ->method('validateClient') ->with('client1', 'secret1', null) ->willReturn(true); @@ -81,7 +81,7 @@ public function testRespondToRequestForRefreshToken(): void $handler = $this->getMockBuilder(TokenIntrospectionHandler::class)->onlyMethods(['validateRefreshToken'])->getMock(); $handler->setClientRepository($clientRepository); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateRefreshToken') ->with($request, 'token1', $client) ->willReturn(['refresh_token', ['refresh_token_id' => 'refresh1']]); @@ -104,12 +104,12 @@ public function testRespondToRequestForInvalidToken(): void $client->setIdentifier('client1'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once()) + $clientRepository->expects(self::once()) ->method('getClientEntity') ->with('client1') ->willReturn($client); $clientRepository - ->expects($this->once()) + ->expects(self::once()) ->method('validateClient') ->with('client1', 'secret1', null) ->willReturn(true); @@ -123,11 +123,11 @@ public function testRespondToRequestForInvalidToken(): void $handler = $this->getMockBuilder(TokenIntrospectionHandler::class) ->onlyMethods(['validateAccessToken', 'validateRefreshToken'])->getMock(); $handler->setClientRepository($clientRepository); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateAccessToken') ->with($request, 'token1', $client) ->willReturn(null); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateRefreshToken') ->with($request, 'token1', $client) ->willReturn(null); @@ -147,12 +147,12 @@ public function testSetResponseType(): void $client->setIdentifier('client1'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once()) + $clientRepository->expects(self::once()) ->method('getClientEntity') ->with('client1') ->willReturn($client); $clientRepository - ->expects($this->once()) + ->expects(self::once()) ->method('validateClient') ->with('client1', 'secret1', null) ->willReturn(true); @@ -166,15 +166,15 @@ public function testSetResponseType(): void $response = new Response(); $responseType = $this->createMock(IntrospectionResponseTypeInterface::class); - $responseType->expects($this->once())->method('setActive')->with(true); - $responseType->expects($this->once())->method('setTokenType')->with('foo'); - $responseType->expects($this->once())->method('setToken')->with(['bar' => 'baz']); - $responseType->expects($this->once())->method('generateHttpResponse')->with($response)->willReturnArgument(0); + $responseType->expects(self::once())->method('setActive')->with(true); + $responseType->expects(self::once())->method('setTokenType')->with('foo'); + $responseType->expects(self::once())->method('setToken')->with(['bar' => 'baz']); + $responseType->expects(self::once())->method('generateHttpResponse')->with($response)->willReturnArgument(0); $handler = $this->getMockBuilder(TokenIntrospectionHandler::class)->onlyMethods(['validateToken'])->getMock(); $handler->setClientRepository($clientRepository); $handler->setResponseType($responseType); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateToken') ->with($request, $client) ->willReturn(['foo', ['bar' => 'baz']]); diff --git a/tests/Handlers/TokenRevocationHandlerTest.php b/tests/Handlers/TokenRevocationHandlerTest.php index d3c4d02d1..a34063577 100644 --- a/tests/Handlers/TokenRevocationHandlerTest.php +++ b/tests/Handlers/TokenRevocationHandlerTest.php @@ -22,18 +22,18 @@ public function testRespondToRequestForAccessToken(): void $client->setIdentifier('client1'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once()) + $clientRepository->expects(self::once()) ->method('getClientEntity') ->with('client1') ->willReturn($client); $clientRepository - ->expects($this->once()) + ->expects(self::once()) ->method('validateClient') ->with('client1', 'secret1', null) ->willReturn(true); $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); - $accessTokenRepository->expects($this->once())->method('revokeAccessToken')->with('access1'); + $accessTokenRepository->expects(self::once())->method('revokeAccessToken')->with('access1'); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'client1', @@ -44,7 +44,7 @@ public function testRespondToRequestForAccessToken(): void $handler = $this->getMockBuilder(TokenRevocationHandler::class)->onlyMethods(['validateAccessToken'])->getMock(); $handler->setClientRepository($clientRepository); $handler->setAccessTokenRepository($accessTokenRepository); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateAccessToken') ->with($request, 'token1', $client) ->willReturn(['access_token', ['jti' => 'access1']]); @@ -62,21 +62,21 @@ public function testRespondToRequestForRefreshToken(): void $client->setIdentifier('client1'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once()) + $clientRepository->expects(self::once()) ->method('getClientEntity') ->with('client1') ->willReturn($client); $clientRepository - ->expects($this->once()) + ->expects(self::once()) ->method('validateClient') ->with('client1', 'secret1', null) ->willReturn(true); $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); - $accessTokenRepository->expects($this->once())->method('revokeAccessToken')->with('access1'); + $accessTokenRepository->expects(self::once())->method('revokeAccessToken')->with('access1'); $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); - $refreshTokenRepository->expects($this->once())->method('revokeRefreshToken')->with('refresh1'); + $refreshTokenRepository->expects(self::once())->method('revokeRefreshToken')->with('refresh1'); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'client1', @@ -88,7 +88,7 @@ public function testRespondToRequestForRefreshToken(): void $handler->setClientRepository($clientRepository); $handler->setAccessTokenRepository($accessTokenRepository); $handler->setRefreshTokenRepository($refreshTokenRepository); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateRefreshToken') ->with($request, 'token1', $client) ->willReturn(['refresh_token', ['refresh_token_id' => 'refresh1', 'access_token_id' => 'access1']]); @@ -106,21 +106,21 @@ public function testRespondToRequestForInvalidToken(): void $client->setIdentifier('client1'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once()) + $clientRepository->expects(self::once()) ->method('getClientEntity') ->with('client1') ->willReturn($client); $clientRepository - ->expects($this->once()) + ->expects(self::once()) ->method('validateClient') ->with('client1', 'secret1', null) ->willReturn(true); $accessTokenRepository = $this->createMock(AccessTokenRepositoryInterface::class); - $accessTokenRepository->expects($this->never())->method('revokeAccessToken'); + $accessTokenRepository->expects(self::never())->method('revokeAccessToken'); $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); - $refreshTokenRepository->expects($this->never())->method('revokeRefreshToken'); + $refreshTokenRepository->expects(self::never())->method('revokeRefreshToken'); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'client1', @@ -133,11 +133,11 @@ public function testRespondToRequestForInvalidToken(): void $handler->setClientRepository($clientRepository); $handler->setAccessTokenRepository($accessTokenRepository); $handler->setRefreshTokenRepository($refreshTokenRepository); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateAccessToken') ->with($request, 'token1', $client) ->willReturn(null); - $handler->expects($this->once()) + $handler->expects(self::once()) ->method('validateRefreshToken') ->with($request, 'token1', $client) ->willReturn(null); diff --git a/tests/ResponseTypes/IntrospectionResponseTest.php b/tests/ResponseTypes/IntrospectionResponseTest.php index 35bcc225c..19c325941 100644 --- a/tests/ResponseTypes/IntrospectionResponseTest.php +++ b/tests/ResponseTypes/IntrospectionResponseTest.php @@ -114,7 +114,7 @@ public function testGenerateHttpResponseWithExtraParams(): void 'scopes' => ['scope1', 'scope2'], 'client_id' => 'client1', 'jti' => null, - 'extension' => 'extension1' + 'extension' => 'extension1', ]); $response = $responseType->generateHttpResponse(new Response()); @@ -130,7 +130,7 @@ public function testGenerateHttpResponseWithExtraParams(): void 'client_id' => 'client1', 'token_type' => 'Bearer', 'foo' => 'bar', - 'extended' => 'extension1' + 'extended' => 'extension1', ], json_decode($response->getBody()->getContents(), true)); } } diff --git a/tests/TokenServerTest.php b/tests/TokenServerTest.php index 1549be28c..64e097a9f 100644 --- a/tests/TokenServerTest.php +++ b/tests/TokenServerTest.php @@ -27,7 +27,7 @@ public function testRespondToTokenRevocationRequest(): void $client->setIdentifier('foo'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once())->method('getClientEntity') + $clientRepository->expects(self::once())->method('getClientEntity') ->with('foo') ->willReturn($client); @@ -50,7 +50,7 @@ public function testRespondToTokenIntrospectionRequest(): void $client->setIdentifier('foo'); $clientRepository = $this->createMock(ClientRepositoryInterface::class); - $clientRepository->expects($this->once())->method('getClientEntity') + $clientRepository->expects(self::once())->method('getClientEntity') ->with('foo') ->willReturn($client); @@ -78,7 +78,7 @@ public function testSetTokenRevocationHandler(): void $response = $this->createMock(ResponseInterface::class); $revocationHandler = $this->getMockBuilder(TokenHandlerInterface::class)->getMock(); - $revocationHandler->expects($this->once())->method('respondToRequest') + $revocationHandler->expects(self::once())->method('respondToRequest') ->with($request, $response) ->willReturn($response); @@ -97,7 +97,7 @@ public function testSetTokenIntrospectionHandler(): void $response = $this->createMock(ResponseInterface::class); $introspectionHandler = $this->getMockBuilder(TokenHandlerInterface::class)->getMock(); - $introspectionHandler->expects($this->once())->method('respondToRequest') + $introspectionHandler->expects(self::once())->method('respondToRequest') ->with($request, $response) ->willReturn($response); From 3c574f235bd6f1b98897e681464a50389237a0c0 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 19 Feb 2025 19:16:58 +0330 Subject: [PATCH 8/8] formatting --- src/ResponseTypes/IntrospectionResponse.php | 4 +- tests/Handlers/AbstractTokenHandlerTest.php | 160 ++++++------------ .../TokenIntrospectionHandlerTest.php | 2 + 3 files changed, 59 insertions(+), 107 deletions(-) diff --git a/src/ResponseTypes/IntrospectionResponse.php b/src/ResponseTypes/IntrospectionResponse.php index 75b951a6f..f1ac23201 100644 --- a/src/ResponseTypes/IntrospectionResponse.php +++ b/src/ResponseTypes/IntrospectionResponse.php @@ -90,7 +90,7 @@ protected function parseParams(string $tokenType, array $token): array 'aud' => $token['aud'] ?? null, 'iss' => $token['iss'] ?? null, 'jti' => $token['jti'] ?? null, - ]); + ], fn ($value) => !is_null($value)); } elseif ($tokenType === 'refresh_token') { return array_filter([ 'scope' => implode(' ', $token['scopes'] ?? []), @@ -98,7 +98,7 @@ protected function parseParams(string $tokenType, array $token): array 'exp' => isset($token['expire_time']) ? $this->convertTimestamp($token['expire_time']) : null, 'sub' => $token['user_id'] ?? null, 'jti' => $token['refresh_token_id'] ?? null, - ]); + ], fn ($value) => !is_null($value)); } else { return []; } diff --git a/tests/Handlers/AbstractTokenHandlerTest.php b/tests/Handlers/AbstractTokenHandlerTest.php index d0f177518..d2ac94396 100644 --- a/tests/Handlers/AbstractTokenHandlerTest.php +++ b/tests/Handlers/AbstractTokenHandlerTest.php @@ -14,15 +14,13 @@ use League\OAuth2\Server\AuthorizationValidators\JwtValidatorInterface; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\CryptTrait; -use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Handlers\AbstractTokenHandler; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use LeagueTests\Stubs\ClientEntity; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; use function base64_encode; use function random_bytes; @@ -62,11 +60,10 @@ public function testSetJwtValidator(): void public function testValidateToken(): void { $client = new ClientEntity(); - $request = new ServerRequest(); + try { - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken()); + (fn () => $this->validateToken($request, $client))->call($this->getAbstractTokenHandlerWithToken()); self::fail('The expected exception was not thrown'); } catch (OAuthServerException $e) { @@ -74,67 +71,43 @@ public function testValidateToken(): void } $request = (new ServerRequest())->withParsedBody(['token' => 'token1']); - self::assertSame( - ['access_token', ['foo' => 'bar']], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken(accessTokenArray: ['foo' => 'bar'], refreshTokenArray: ['bar' => 'foo'])) - ); - - $request = (new ServerRequest())->withParsedBody(['token' => 'token1']); - self::assertSame( - ['refresh_token', ['foo' => 'bar']], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken(refreshTokenArray: ['foo' => 'bar'])) - ); - - $request = (new ServerRequest())->withParsedBody(['token' => 'token1']); - self::assertSame( - [null, null], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken()) - ); - $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'access_token']); - self::assertSame( - ['access_token', ['foo' => 'bar']], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken(accessTokenArray: ['foo' => 'bar'], refreshTokenArray: ['bar' => 'foo'])) - ); - - $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'access_token']); - self::assertSame( - ['refresh_token', ['bar' => 'foo']], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken(refreshTokenArray: ['bar' => 'foo'])) - ); + self::assertSame(['access_token', ['foo' => 'bar']], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken(accessToken: ['foo' => 'bar'], refreshToken: ['bar' => 'foo']) + )); + self::assertSame(['access_token', ['foo' => 'bar']], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken(accessToken: ['foo' => 'bar']) + )); + self::assertSame(['refresh_token', ['bar' => 'foo']], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken(refreshToken: ['bar' => 'foo']) + )); + self::assertSame([null, null], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken() + )); $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'access_token']); - self::assertSame( - [null, null], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken()) - ); - $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'refresh_token']); - self::assertSame( - ['refresh_token', ['bar' => 'foo']], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken(accessTokenArray: ['foo' => 'bar'], refreshTokenArray: ['bar' => 'foo'])) - ); + self::assertSame(['access_token', ['foo' => 'bar']], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken(accessToken: ['foo' => 'bar'], refreshToken: ['bar' => 'foo']) + )); + self::assertSame(['refresh_token', ['bar' => 'foo']], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken(refreshToken: ['bar' => 'foo']) + )); + self::assertSame([null, null], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken() + )); $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'refresh_token']); - self::assertSame( - ['access_token', ['foo' => 'bar']], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken(accessTokenArray: ['foo' => 'bar'])) - ); - $request = (new ServerRequest())->withParsedBody(['token' => 'token1', 'token_type_hint' => 'refresh_token']); - self::assertSame( - [null, null], - (fn () => $this->validateToken($request, $client)) - ->call($this->getAbstractTokenHandlerWithToken()) - ); + self::assertSame(['refresh_token', ['bar' => 'foo']], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken(accessToken: ['foo' => 'bar'], refreshToken: ['bar' => 'foo']) + )); + self::assertSame(['access_token', ['foo' => 'bar']], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken(accessToken: ['foo' => 'bar']) + )); + self::assertSame([null, null], (fn () => $this->validateToken($request, $client))->call( + $this->getAbstractTokenHandlerWithToken() + )); } public function testValidateAccessToken(): void @@ -384,14 +357,12 @@ public function testValidateRefreshTokenWithInvalidToken(): void self::assertNull($result); } - private function getAbstractTokenHandler(): AbstractTokenHandler + /** + * @return AbstractTokenHandler&MockObject + */ + private function getAbstractTokenHandler(): MockObject { - $handler = new class () extends AbstractTokenHandler { - public function respondToRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface - { - return $response; - } - }; + $handler = $this->getMockBuilder(AbstractTokenHandler::class)->onlyMethods(['respondToRequest'])->getMock(); $handler->setEncryptionKey($this->encryptionKey); $handler->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); @@ -400,45 +371,24 @@ public function respondToRequest(ServerRequestInterface $request, ResponseInterf } /** - * @param array|null $accessTokenArray - * @param array|null $refreshTokenArray + * @param array|null $accessToken + * @param array|null $refreshToken + * + * @return AbstractTokenHandler&MockObject */ - private function getAbstractTokenHandlerWithToken( - ?array $accessTokenArray = null, - ?array $refreshTokenArray = null, - ): AbstractTokenHandler { - return new class ($accessTokenArray, $refreshTokenArray) extends AbstractTokenHandler { - /** - * @param array|null $accessTokenArray - * @param array|null $refreshTokenArray - */ - public function __construct( - private ?array $accessTokenArray = null, - private ?array $refreshTokenArray = null - ) { - } - - public function respondToRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface - { - return $response; - } - - /** - * {@inheritdoc} - */ - protected function validateAccessToken(ServerRequestInterface $request, string $accessToken, ClientEntityInterface $client): ?array - { - return isset($this->accessTokenArray) ? ['access_token', [...$this->accessTokenArray]] : null; - } - - /** - * {@inheritdoc} - */ - protected function validateRefreshToken(ServerRequestInterface $request, string $refreshToken, ClientEntityInterface $client): ?array - { - return isset($this->refreshTokenArray) ? ['refresh_token', [...$this->refreshTokenArray]] : null; - } - }; + private function getAbstractTokenHandlerWithToken(?array $accessToken = null, ?array $refreshToken = null): MockObject + { + $handler = $this->getMockBuilder(AbstractTokenHandler::class) + ->onlyMethods(['respondToRequest', 'validateAccessToken', 'validateRefreshToken']) + ->getMock(); + + $handler->method('validateAccessToken') + ->willReturn($accessToken === null ? null : ['access_token', $accessToken]); + + $handler->method('validateRefreshToken') + ->willReturn($refreshToken === null ? null : ['refresh_token', $refreshToken]); + + return $handler; } /** diff --git a/tests/Handlers/TokenIntrospectionHandlerTest.php b/tests/Handlers/TokenIntrospectionHandlerTest.php index e0c894da4..4a5be8194 100644 --- a/tests/Handlers/TokenIntrospectionHandlerTest.php +++ b/tests/Handlers/TokenIntrospectionHandlerTest.php @@ -51,6 +51,7 @@ public function testRespondToRequestForAccessToken(): void self::assertSame('application/json; charset=UTF-8', $response->getHeaderLine('Content-Type')); self::assertSame([ 'active' => true, + 'scope' => '', 'token_type' => 'Bearer', 'jti' => 'access1', ], json_decode($response->getBody()->getContents(), true)); @@ -93,6 +94,7 @@ public function testRespondToRequestForRefreshToken(): void self::assertSame('application/json; charset=UTF-8', $response->getHeaderLine('Content-Type')); self::assertSame([ 'active' => true, + 'scope' => '', 'jti' => 'refresh1', ], json_decode($response->getBody()->getContents(), true)); }