diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c0d6dfaf9..1ea5cfe84 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,6 +44,6 @@ jobs: run: vendor/bin/phpunit --coverage-clover=coverage.clover - name: Code coverage - if: ${{ github.ref == 'refs/heads/master' && github.repository == 'thephpleague/oauth2-server' }} + if: ${{ github.ref == 'refs/heads/master' && github.repository == 'thephpleague/oauth2-server' && startsWith(matrix.os, 'ubuntu') }} run: ~/.composer/vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover diff --git a/CHANGELOG.md b/CHANGELOG.md index 4899ef506..0d16d2b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [9.2.0] - released 2025-02-15 +### Added +- Added a new function to the provided ClientTrait, `supportsGrantType` to allow the auth server to issue the response `unauthorized_client` when applicable (PR #1420) + +### Fixed +- Fix a bug on setting interval visibility of device authorization grant (PR #1410) +- Fix a bug where the new poll date were not persisted when `slow_down` error happens, because the exception is thrown before calling `persistDeviceCode`. (PR #1410) +- Fix a bug where `slow_down` error response may have been returned even after the user has completed the auth flow (already approved / denied the request). (PR #1410) +- Clients only validated for Refresh, Device Code, and Password grants if the client is confidential (PR #1420) +- Emit `RequestAccessTokenEvent` and `RequestRefreshTokenEvent` events instead of the general `RequestEvent` event when an access / refresh token is issued using device authorization grant. (PR #1467) + ### Changed - Key permission checks ignored on Windows regardless of userland choice as cannot be run successfully on this OS (PR #1447) @@ -16,9 +27,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - In the Auth Code grant, when requesting an access token with an invalid auth code, we now respond with an invalid_grant error instead of invalid_request (PR #1433) - Fixed spec compliance issue where device access token request was mistakenly expecting to receive scopes in the request (PR #1412) - Refresh tokens pre version 9 might have had user IDs set as ints which meant they were incorrectly rejected. We now cast these values to strings to allow old refresh tokens (PR #1436) -- Fixed bug on setting interval visibility of device authorization grant (PR #1410) -- Fix a bug where the new poll date were not persisted when `slow_down` error happens, because the exception is thrown before calling `persistDeviceCode`. (PR #1410) -- Fix a bug where `slow_down` error response may have been returned even after the user has completed the auth flow (already approved / denied the request). (PR #1410) ## [9.0.1] - released 2024-10-14 ### Fixed @@ -669,7 +677,8 @@ Version 5 is a complete code rewrite. - First major release -[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/9.1.0...HEAD +[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/9.2.0...HEAD +[9.2.0]: https://github.com/thephpleague/oauth2-server/compare/9.1.0...9.2.0 [9.1.0]: https://github.com/thephpleague/oauth2-server/compare/9.0.1...9.1.0 [9.0.1]: https://github.com/thephpleague/oauth2-server/compare/9.0.0...9.0.1 [9.0.0]: https://github.com/thephpleague/oauth2-server/compare/9.0.0-RC1...9.0.0 diff --git a/examples/README.md b/examples/README.md index 69df9cef6..48b6fb8c4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,12 +1,11 @@ -# Example implementations +# Example implementations (via [`Slim 3`](https://github.com/slimphp/Slim/tree/3.x)) ## Installation 0. Run `composer install` in this directory to install dependencies 0. Create a private key `openssl genrsa -out private.key 2048` -0. Create a public key `openssl rsa -in private.key -pubout > public.key` -0. `cd` into the public directory -0. Start a PHP server `php -S localhost:4444` +0. Export the public key `openssl rsa -in private.key -pubout > public.key` +0. Start local PHP server `php -S 127.0.0.1:4444 -t public/` ## Testing the client credentials grant example diff --git a/examples/src/Repositories/AccessTokenRepository.php b/examples/src/Repositories/AccessTokenRepository.php index 1eb3e5bdd..e04b3e527 100644 --- a/examples/src/Repositories/AccessTokenRepository.php +++ b/examples/src/Repositories/AccessTokenRepository.php @@ -14,6 +14,7 @@ use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use OAuth2ServerExamples\Entities\AccessTokenEntity; @@ -46,20 +47,17 @@ public function isAccessTokenRevoked($tokenId): bool /** * {@inheritdoc} */ - public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null): AccessTokenEntityInterface + public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, ?UserEntityInterface $user = null): AccessTokenEntityInterface { $accessToken = new AccessTokenEntity(); $accessToken->setClient($clientEntity); + $accessToken->setUser($user); foreach ($scopes as $scope) { $accessToken->addScope($scope); } - if ($userIdentifier !== null) { - $accessToken->setUserIdentifier((string) $userIdentifier); - } - return $accessToken; } } diff --git a/examples/src/Repositories/AuthCodeRepository.php b/examples/src/Repositories/AuthCodeRepository.php index 962ed8da9..4d21a30e7 100644 --- a/examples/src/Repositories/AuthCodeRepository.php +++ b/examples/src/Repositories/AuthCodeRepository.php @@ -21,7 +21,7 @@ class AuthCodeRepository implements AuthCodeRepositoryInterface /** * {@inheritdoc} */ - public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity) + public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): void { // Some logic to persist the auth code to a database } @@ -29,7 +29,7 @@ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity) /** * {@inheritdoc} */ - public function revokeAuthCode($codeId) + public function revokeAuthCode($codeId): void { // Some logic to revoke the auth code in a database } @@ -37,7 +37,7 @@ public function revokeAuthCode($codeId) /** * {@inheritdoc} */ - public function isAuthCodeRevoked($codeId) + public function isAuthCodeRevoked($codeId): bool { return false; // The auth code has not been revoked } @@ -45,7 +45,7 @@ public function isAuthCodeRevoked($codeId) /** * {@inheritdoc} */ - public function getNewAuthCode() + public function getNewAuthCode(): AuthCodeEntityInterface { return new AuthCodeEntity(); } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 7949ac1d8..0e1613816 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,4 +2,8 @@ parameters: level: 8 paths: - src - - tests \ No newline at end of file + - tests + ignoreErrors: + - + message: '#Deprecated since v5.5, please use {@see self::withValidationConstraints\(\)} instead#' + reportUnmatched: false diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index c894bbd6b..9a7e7e011 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -14,6 +14,7 @@ use DateInterval; use Defuse\Crypto\Key; +use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\EventEmitting\EmitterAwareInterface; use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill; use League\OAuth2\Server\Exception\OAuthServerException; @@ -42,14 +43,8 @@ class AuthorizationServer implements EmitterAwareInterface */ protected array $grantTypeAccessTokenTTL = []; - protected CryptKeyInterface $privateKey; - - protected CryptKeyInterface $publicKey; - protected ResponseTypeInterface $responseType; - private string|Key $encryptionKey; - private string $defaultScope = ''; private bool $revokeRefreshTokens = true; @@ -61,17 +56,8 @@ public function __construct( private ClientRepositoryInterface $clientRepository, private AccessTokenRepositoryInterface $accessTokenRepository, private ScopeRepositoryInterface $scopeRepository, - CryptKeyInterface|string $privateKey, - Key|string $encryptionKey, ResponseTypeInterface|null $responseType = null ) { - if ($privateKey instanceof CryptKeyInterface === false) { - $privateKey = new CryptKey($privateKey); - } - - $this->privateKey = $privateKey; - $this->encryptionKey = $encryptionKey; - if ($responseType === null) { $responseType = new BearerTokenResponse(); } else { @@ -94,9 +80,7 @@ public function enableGrantType(GrantTypeInterface $grantType, DateInterval|null $grantType->setClientRepository($this->clientRepository); $grantType->setScopeRepository($this->scopeRepository); $grantType->setDefaultScope($this->defaultScope); - $grantType->setPrivateKey($this->privateKey); $grantType->setEmitter($this->getEmitter()); - $grantType->setEncryptionKey($this->encryptionKey); $grantType->revokeRefreshTokens($this->revokeRefreshTokens); $this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType; @@ -152,10 +136,10 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ /** * Complete a device authorization request */ - public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void + public function completeDeviceAuthorizationRequest(string $deviceCode, UserEntityInterface $user, bool $userApproved): void { $this->enabledGrantTypes['urn:ietf:params:oauth:grant-type:device_code'] - ->completeDeviceAuthorizationRequest($deviceCode, $userId, $userApproved); + ->completeDeviceAuthorizationRequest($deviceCode, $user, $userApproved); } /** @@ -187,15 +171,7 @@ public function respondToAccessTokenRequest(ServerRequestInterface $request, Res */ protected function getResponseType(): ResponseTypeInterface { - $responseType = clone $this->responseType; - - if ($responseType instanceof AbstractResponseType) { - $responseType->setPrivateKey($this->privateKey); - } - - $responseType->setEncryptionKey($this->encryptionKey); - - return $responseType; + return clone $this->responseType; } /** diff --git a/src/AuthorizationValidators/BearerTokenValidator.php b/src/AuthorizationValidators/BearerTokenValidator.php index 0442dd48e..5080d0475 100644 --- a/src/AuthorizationValidators/BearerTokenValidator.php +++ b/src/AuthorizationValidators/BearerTokenValidator.php @@ -36,8 +36,6 @@ class BearerTokenValidator implements AuthorizationValidatorInterface { - use CryptTrait; - protected CryptKeyInterface $publicKey; private Configuration $jwtConfiguration; diff --git a/src/CryptTrait.php b/src/CryptTrait.php deleted file mode 100644 index ee481b55c..000000000 --- a/src/CryptTrait.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @copyright Copyright (c) Alex Bilbie - * @license http://mit-license.org/ - * - * @link https://github.com/thephpleague/oauth2-server - */ - -declare(strict_types=1); - -namespace League\OAuth2\Server; - -use Defuse\Crypto\Crypto; -use Defuse\Crypto\Exception\EnvironmentIsBrokenException; -use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; -use Defuse\Crypto\Key; -use Exception; -use InvalidArgumentException; -use LogicException; - -use function is_string; - -trait CryptTrait -{ - protected string|Key|null $encryptionKey = null; - - /** - * Encrypt data with encryptionKey. - * - * @throws LogicException - */ - protected function encrypt(string $unencryptedData): string - { - try { - if ($this->encryptionKey instanceof Key) { - return Crypto::encrypt($unencryptedData, $this->encryptionKey); - } - - if (is_string($this->encryptionKey)) { - return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey); - } - - throw new LogicException('Encryption key not set when attempting to encrypt'); - } catch (Exception $e) { - throw new LogicException($e->getMessage(), 0, $e); - } - } - - /** - * Decrypt data with encryptionKey. - * - * @throws LogicException - */ - protected function decrypt(string $encryptedData): string - { - try { - if ($this->encryptionKey instanceof Key) { - return Crypto::decrypt($encryptedData, $this->encryptionKey); - } - - if (is_string($this->encryptionKey)) { - return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey); - } - - throw new LogicException('Encryption key not set when attempting to decrypt'); - } catch (WrongKeyOrModifiedCiphertextException $e) { - $exceptionMessage = 'The authcode or decryption key/password used ' - . 'is not correct'; - - throw new InvalidArgumentException($exceptionMessage, 0, $e); - } catch (EnvironmentIsBrokenException $e) { - $exceptionMessage = 'Auth code decryption failed. This is likely ' - . 'due to an environment issue or runtime bug in the ' - . 'decryption library'; - - throw new LogicException($exceptionMessage, 0, $e); - } catch (Exception $e) { - throw new LogicException($e->getMessage(), 0, $e); - } - } - - public function setEncryptionKey(Key|string|null $key = null): void - { - $this->encryptionKey = $key; - } -} diff --git a/src/Entities/AccessTokenEntityInterface.php b/src/Entities/AccessTokenEntityInterface.php index 3c998b4d2..301409ece 100644 --- a/src/Entities/AccessTokenEntityInterface.php +++ b/src/Entities/AccessTokenEntityInterface.php @@ -17,12 +17,17 @@ interface AccessTokenEntityInterface extends TokenInterface { /** - * Set a private key used to encrypt the access token. + * Generate a string representation of the access token. */ - public function setPrivateKey(CryptKeyInterface $privateKey): void; + public function toString(): string; /** - * Generate a string representation of the access token. + * Set the algorithm for signing the access token with the given private key + * + * @see https://lcobucci-jwt.readthedocs.io/en/latest/supported-algorithms/ + * + * Symmetric - HS256, HS384, HS512, BLAKE2B + * Asymmetric - ES256, ES384, ES512, RS256, RS384, RS512, EdDSA */ - public function toString(): string; + public function setSigner(string $signerAlgorithm, CryptKeyInterface $privateKey): void; } diff --git a/src/Entities/AuthCodeEntityInterface.php b/src/Entities/AuthCodeEntityInterface.php index 68c6a2f5b..d1d88e154 100644 --- a/src/Entities/AuthCodeEntityInterface.php +++ b/src/Entities/AuthCodeEntityInterface.php @@ -14,7 +14,15 @@ interface AuthCodeEntityInterface extends TokenInterface { - public function getRedirectUri(): string|null; + public function getRedirectUri(): ?string; - public function setRedirectUri(string $uri): void; + public function setRedirectUri(?string $uri): void; + + public function setCodeChallenge(?string $codeChallenge): void; + + public function getCodeChallenge(): ?string; + + public function setCodeChallengeMethod(?string $codeChallengeMethod): void; + + public function getCodeChallengeMethod(): ?string; } diff --git a/src/Entities/ClientEntityInterface.php b/src/Entities/ClientEntityInterface.php index f3838b11c..fc185a6e6 100644 --- a/src/Entities/ClientEntityInterface.php +++ b/src/Entities/ClientEntityInterface.php @@ -38,4 +38,11 @@ public function getRedirectUri(): string|array; * Returns true if the client is confidential. */ public function isConfidential(): bool; + + /* + * Returns true if the client supports the given grant type. + * + * TODO: To be added in a future major release. + */ + // public function supportsGrantType(string $grantType): bool; } diff --git a/src/Entities/RefreshTokenEntityInterface.php b/src/Entities/RefreshTokenEntityInterface.php index 79a99f24c..e558c810a 100644 --- a/src/Entities/RefreshTokenEntityInterface.php +++ b/src/Entities/RefreshTokenEntityInterface.php @@ -12,34 +12,8 @@ namespace League\OAuth2\Server\Entities; -use DateTimeImmutable; - -interface RefreshTokenEntityInterface +interface RefreshTokenEntityInterface extends TokenInterface { - /** - * Get the token's identifier. - * - * @return non-empty-string - */ - public function getIdentifier(): string; - - /** - * Set the token's identifier. - * - * @param non-empty-string $identifier - */ - public function setIdentifier(string $identifier): void; - - /** - * Get the token's expiry date time. - */ - public function getExpiryDateTime(): DateTimeImmutable; - - /** - * Set the date time when the token expires. - */ - public function setExpiryDateTime(DateTimeImmutable $dateTime): void; - /** * Set the access token that the refresh token was associated with. */ diff --git a/src/Entities/TokenInterface.php b/src/Entities/TokenInterface.php index b9d5c270f..3a0be9e65 100644 --- a/src/Entities/TokenInterface.php +++ b/src/Entities/TokenInterface.php @@ -41,18 +41,18 @@ public function getExpiryDateTime(): DateTimeImmutable; public function setExpiryDateTime(DateTimeImmutable $dateTime): void; /** - * Set the identifier of the user associated with the token. + * Set the user associated with the token. * - * @param non-empty-string $identifier + * @param ?UserEntityInterface $identifier The identifier of the user */ - public function setUserIdentifier(string $identifier): void; + public function setUser(?UserEntityInterface $user): void; /** - * Get the token user's identifier. + * Get the token user. * - * @return non-empty-string|null + * @return ?UserEntityInterface */ - public function getUserIdentifier(): string|null; + public function getUser(): ?UserEntityInterface; /** * Get the client that the token was issued to. @@ -75,4 +75,11 @@ public function addScope(ScopeEntityInterface $scope): void; * @return ScopeEntityInterface[] */ public function getScopes(): array; + + /** + * Set the scopes array (doesn't check for duplicates) + * + * @param array scopes + */ + public function setScopes(array $scopes): void; } diff --git a/src/Entities/Traits/AccessTokenTrait.php b/src/Entities/Traits/AccessTokenTrait.php index 6b1387b5f..af8f89782 100644 --- a/src/Entities/Traits/AccessTokenTrait.php +++ b/src/Entities/Traits/AccessTokenTrait.php @@ -15,43 +15,80 @@ use DateTimeImmutable; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; -use Lcobucci\JWT\Signer\Rsa\Sha256; +use Lcobucci\JWT\Signer\Hmac\Sha256 as HS256; +use Lcobucci\JWT\Signer\Hmac\Sha384 as HS384; +use Lcobucci\JWT\Signer\Hmac\Sha512 as HS512; +use Lcobucci\JWT\Signer\Blake2b as BLAKE2B; +use Lcobucci\JWT\Signer\Rsa\Sha256 as RS256; +use Lcobucci\JWT\Signer\Rsa\Sha384 as RS384; +use Lcobucci\JWT\Signer\Rsa\Sha512 as RS512; +use Lcobucci\JWT\Signer\Ecdsa\Sha256 as ES256; +use Lcobucci\JWT\Signer\Ecdsa\Sha384 as ES384; +use Lcobucci\JWT\Signer\Ecdsa\Sha512 as ES512; +use Lcobucci\JWT\Signer\Eddsa as EDDSA; +use Lcobucci\JWT\Encoding\ChainedFormatter; +use Lcobucci\JWT\Encoding\JoseEncoder; use Lcobucci\JWT\Token; +use Lcobucci\JWT\Token\Builder; +use Lcobucci\JWT\Signer; +use Lcobucci\JWT\Signer\Key; use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; -use RuntimeException; +use League\OAuth2\Server\Entities\UserEntityInterface; trait AccessTokenTrait { - private CryptKeyInterface $privateKey; + private ?Key $privateKey = null; + + private Signer $signer; private Configuration $jwtConfiguration; - /** - * Set the private key used to encrypt this access token. - */ - public function setPrivateKey(CryptKeyInterface $privateKey): void + public function __construct() { - $this->privateKey = $privateKey; + $this->signer = new RS256(); } - /** - * Initialise the JWT Configuration. - */ - public function initJwtConfiguration(): void + public function setSigner(string $signerAlgorithm, CryptKeyInterface $privateKey): void { - $privateKeyContents = $this->privateKey->getKeyContents(); - - if ($privateKeyContents === '') { - throw new RuntimeException('Private key is empty'); + $this->privateKey = InMemory::plainText($privateKey->getKeyContents(), $privateKey->getPassPhrase() ?? ''); + + switch (strtoupper($signerAlgorithm)) { + case 'HS256': + $this->signer = new HS256(); + break; + case 'HS384': + $this->signer = new HS384(); + break; + case 'HS512': + $this->signer = new HS512(); + break; + case 'BLAKE2B': + $this->signer = new BLAKE2B(); + break; + case 'ES256': + $this->signer = new ES256(); + break; + case 'ES384': + $this->signer = new ES384(); + break; + case 'ES512': + $this->signer = new ES512(); + break; + case 'RS256': + $this->signer = new RS256(); + break; + case 'RS384': + $this->signer = new RS384(); + break; + case 'RS512': + $this->signer = new RS512(); + break; + case 'EDDSA': + $this->signer = new EDDSA(); + break; } - - $this->jwtConfiguration = Configuration::forAsymmetricSigner( - new Sha256(), - InMemory::plainText($privateKeyContents, $this->privateKey->getPassPhrase() ?? ''), - InMemory::plainText('empty', 'empty') - ); } /** @@ -59,9 +96,9 @@ public function initJwtConfiguration(): void */ private function convertToJWT(): Token { - $this->initJwtConfiguration(); + $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); - return $this->jwtConfiguration->builder() + return $tokenBuilder ->permittedFor($this->getClient()->getIdentifier()) ->identifiedBy($this->getIdentifier()) ->issuedAt(new DateTimeImmutable()) @@ -69,7 +106,7 @@ private function convertToJWT(): Token ->expiresAt($this->getExpiryDateTime()) ->relatedTo($this->getSubjectIdentifier()) ->withClaim('scopes', $this->getScopes()) - ->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey()); + ->getToken($this->signer, $this->privateKey); } /** @@ -77,6 +114,10 @@ private function convertToJWT(): Token */ public function toString(): string { + if ($this->privateKey === null) { + return $this->getIdentifier(); + } + return $this->convertToJWT()->toString(); } @@ -85,9 +126,11 @@ abstract public function getClient(): ClientEntityInterface; abstract public function getExpiryDateTime(): DateTimeImmutable; /** - * @return non-empty-string|null + * Get the token user. + * + * @return ?UserEntityInterface */ - abstract public function getUserIdentifier(): string|null; + abstract public function getUser(): ?UserEntityInterface; /** * @return ScopeEntityInterface[] @@ -104,6 +147,6 @@ abstract public function getIdentifier(): string; */ private function getSubjectIdentifier(): string { - return $this->getUserIdentifier() ?? $this->getClient()->getIdentifier(); + return $this->getUser()?->getIdentifier() ?? $this->getClient()->getIdentifier(); } } diff --git a/src/Entities/Traits/AuthCodeTrait.php b/src/Entities/Traits/AuthCodeTrait.php index 403500b62..8dcacfa82 100644 --- a/src/Entities/Traits/AuthCodeTrait.php +++ b/src/Entities/Traits/AuthCodeTrait.php @@ -16,13 +16,44 @@ trait AuthCodeTrait { protected ?string $redirectUri = null; - public function getRedirectUri(): string|null + + /** + * The code challenge (if provided) + */ + protected ?string $codeChallenge; + + /** + * The code challenge method (if provided) + */ + protected ?string $codeChallengeMethod; + + public function getRedirectUri(): ?string { return $this->redirectUri; } - public function setRedirectUri(string $uri): void + public function setRedirectUri(?string $uri): void { $this->redirectUri = $uri; } + + public function getCodeChallenge(): ?string + { + return $this->codeChallenge ?? null; + } + + public function setCodeChallenge(?string $codeChallenge): void + { + $this->codeChallenge = $codeChallenge; + } + + public function getCodeChallengeMethod(): ?string + { + return $this->codeChallengeMethod ?? null; + } + + public function setCodeChallengeMethod(?string $codeChallengeMethod): void + { + $this->codeChallengeMethod = $codeChallengeMethod; + } } diff --git a/src/Entities/Traits/ClientTrait.php b/src/Entities/Traits/ClientTrait.php index b179cfac4..ada53fa5a 100644 --- a/src/Entities/Traits/ClientTrait.php +++ b/src/Entities/Traits/ClientTrait.php @@ -52,4 +52,12 @@ public function isConfidential(): bool { return $this->isConfidential; } + + /** + * Returns true if the client supports the given grant type. + */ + public function supportsGrantType(string $grantType): bool + { + return true; + } } diff --git a/src/Entities/Traits/RefreshTokenTrait.php b/src/Entities/Traits/RefreshTokenTrait.php index a0d4c8885..7c7235c0e 100644 --- a/src/Entities/Traits/RefreshTokenTrait.php +++ b/src/Entities/Traits/RefreshTokenTrait.php @@ -12,15 +12,12 @@ namespace League\OAuth2\Server\Entities\Traits; -use DateTimeImmutable; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; trait RefreshTokenTrait { protected AccessTokenEntityInterface $accessToken; - protected DateTimeImmutable $expiryDateTime; - /** * {@inheritdoc} */ @@ -36,20 +33,4 @@ public function getAccessToken(): AccessTokenEntityInterface { return $this->accessToken; } - - /** - * Get the token's expiry date time. - */ - public function getExpiryDateTime(): DateTimeImmutable - { - return $this->expiryDateTime; - } - - /** - * Set the date time when the token expires. - */ - public function setExpiryDateTime(DateTimeImmutable $dateTime): void - { - $this->expiryDateTime = $dateTime; - } } diff --git a/src/Entities/Traits/TokenEntityTrait.php b/src/Entities/Traits/TokenEntityTrait.php index ad472acd9..f1c8b61ef 100644 --- a/src/Entities/Traits/TokenEntityTrait.php +++ b/src/Entities/Traits/TokenEntityTrait.php @@ -15,6 +15,7 @@ use DateTimeImmutable; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; use function array_values; @@ -28,9 +29,9 @@ trait TokenEntityTrait protected DateTimeImmutable $expiryDateTime; /** - * @var non-empty-string|null + * @var ?UserEntityInterface */ - protected string|null $userIdentifier = null; + protected ?UserEntityInterface $user = null; protected ClientEntityInterface $client; @@ -52,6 +53,16 @@ public function getScopes(): array return array_values($this->scopes); } + /** + * Set the scopes array (doesn't check for duplicates) + * + * @param array scopes + */ + public function setScopes(array $scopes): void + { + $this->scopes = $scopes; + } + /** * Get the token's expiry date time. */ @@ -69,23 +80,23 @@ public function setExpiryDateTime(DateTimeImmutable $dateTime): void } /** - * Set the identifier of the user associated with the token. + * Set the user associated with the token. * - * @param non-empty-string $identifier The identifier of the user + * @param ?UserEntityInterface $identifier The identifier of the user */ - public function setUserIdentifier(string $identifier): void + public function setUser(?UserEntityInterface $user): void { - $this->userIdentifier = $identifier; + $this->user = $user; } /** - * Get the token user's identifier. + * Get the token user. * - * @return non-empty-string|null + * @return ?UserEntityInterface */ - public function getUserIdentifier(): string|null + public function getUser(): ?UserEntityInterface { - return $this->userIdentifier; + return $this->user; } /** diff --git a/src/Exception/OAuthServerException.php b/src/Exception/OAuthServerException.php index 24a38d3fe..df8b58a3d 100644 --- a/src/Exception/OAuthServerException.php +++ b/src/Exception/OAuthServerException.php @@ -252,8 +252,17 @@ public static function slowDown(string $hint = '', ?Throwable $previous = null): } /** + * Unauthorized client error. + */ + public static function unauthorizedClient(?string $hint = null): static { - return $this->errorType; + return new static( + 'The authenticated client is not authorized to use this authorization grant type.', + 14, + 'unauthorized_client', + 400, + $hint + ); } /** diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index 5ab81ff77..005206f97 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -26,6 +26,7 @@ use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\EventEmitting\EmitterAwarePolyfill; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; @@ -60,7 +61,6 @@ abstract class AbstractGrant implements GrantTypeInterface { use EmitterAwarePolyfill; - use CryptTrait; protected const SCOPE_DELIMITER_STRING = ' '; @@ -80,8 +80,6 @@ abstract class AbstractGrant implements GrantTypeInterface protected DateInterval $refreshTokenTTL; - protected CryptKeyInterface $privateKey; - protected string $defaultScope; protected bool $revokeRefreshTokens = true; @@ -124,14 +122,6 @@ public function setRefreshTokenTTL(DateInterval $refreshTokenTTL): void $this->refreshTokenTTL = $refreshTokenTTL; } - /** - * Set the private key - */ - public function setPrivateKey(CryptKeyInterface $privateKey): void - { - $this->privateKey = $privateKey; - } - public function setDefaultScope(string $scope): void { $this->defaultScope = $scope; @@ -151,18 +141,18 @@ protected function validateClient(ServerRequestInterface $request): ClientEntity { [$clientId, $clientSecret] = $this->getClientCredentials($request); - if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) { - $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); - - throw OAuthServerException::invalidClient($request); - } $client = $this->getClientEntityOrFail($clientId, $request); - // If a redirect URI is provided ensure it matches what is pre-registered - $redirectUri = $this->getRequestParameter('redirect_uri', $request); + if ($client->isConfidential()) { + if ($clientSecret === '') { + throw OAuthServerException::invalidRequest('client_secret'); + } - if ($redirectUri !== null) { - $this->validateRedirectUri($redirectUri, $client, $request); + 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; @@ -189,9 +179,22 @@ protected function getClientEntityOrFail(string $clientId, ServerRequestInterfac throw OAuthServerException::invalidClient($request); } + if ($this->supportsGrantType($client, $this->getIdentifier()) === false) { + throw OAuthServerException::unauthorizedClient(); + } + return $client; } + /** + * Returns true if the given client is authorized to use the given grant type. + */ + protected function supportsGrantType(ClientEntityInterface $client, string $grantType): bool + { + return method_exists($client, 'supportsGrantType') === false + || $client->supportsGrantType($grantType) === true; + } + /** * Gets the client credentials from the request from the request body or * the Http Basic Authorization header @@ -260,10 +263,10 @@ public function validateScopes(string|array|null $scopes, ?string $redirectUri = throw OAuthServerException::invalidScope($scopeItem, $redirectUri); } - $validScopes[] = $scope; + $validScopes[$scopeItem] = $scope; } - return $validScopes; + return array_values($validScopes); } /** @@ -404,14 +407,13 @@ protected function getServerParameter(string $parameter, ServerRequestInterface protected function issueAccessToken( DateInterval $accessTokenTTL, ClientEntityInterface $client, - string|null $userIdentifier, + ?UserEntityInterface $user, array $scopes = [] ): AccessTokenEntityInterface { $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; - $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier); + $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $user); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL)); - $accessToken->setPrivateKey($this->privateKey); while ($maxGenerationAttempts-- > 0) { $accessToken->setIdentifier($this->generateUniqueIdentifier()); @@ -433,7 +435,7 @@ protected function issueAccessToken( /** * Issue an auth code. * - * @param non-empty-string $userIdentifier + * @param ?UserEntityInterface $user * @param ScopeEntityInterface[] $scopes * * @throws OAuthServerException @@ -442,16 +444,20 @@ protected function issueAccessToken( protected function issueAuthCode( DateInterval $authCodeTTL, ClientEntityInterface $client, - string $userIdentifier, + ?UserEntityInterface $user, ?string $redirectUri, - array $scopes = [] + array $scopes = [], + ?string $codeChallenge = null, + ?string $codeChallengeMethod = null ): AuthCodeEntityInterface { $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; $authCode = $this->authCodeRepository->getNewAuthCode(); $authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL)); $authCode->setClient($client); - $authCode->setUserIdentifier($userIdentifier); + $authCode->setUser($user); + $authCode->setCodeChallenge($codeChallenge); + $authCode->setCodeChallengeMethod($codeChallengeMethod); if ($redirectUri !== null) { $authCode->setRedirectUri($redirectUri); @@ -484,6 +490,10 @@ protected function issueAuthCode( */ protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface { + if ($this->supportsGrantType($accessToken->getClient(), 'refresh_token') === false) { + return null; + } + $refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); if ($refreshToken === null) { @@ -593,7 +603,7 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ /** * {@inheritdoc} */ - public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void + public function completeDeviceAuthorizationRequest(string $deviceCode, UserEntityInterface $user, bool $userApproved): void { throw new LogicException('This grant cannot complete a device authorization request'); } diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index 9fb5271c3..e1c3f552b 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -19,6 +19,7 @@ use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface; use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier; use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier; +use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; @@ -32,7 +33,6 @@ use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use LogicException; use Psr\Http\Message\ServerRequestInterface; -use stdClass; use function array_key_exists; use function array_keys; @@ -45,7 +45,6 @@ use function json_decode; use function json_encode; use function preg_match; -use function property_exists; use function sprintf; use function time; @@ -97,55 +96,55 @@ public function respondToAccessTokenRequest( ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { - list($clientId) = $this->getClientCredentials($request); + $client = $this->validateClient($request); - $client = $this->getClientEntityOrFail($clientId, $request); + $code = $this->getRequestParameter('code', $request); - // Only validate the client if it is confidential - if ($client->isConfidential()) { - $this->validateClient($request); + if ($code === null) { + throw OAuthServerException::invalidRequest('code'); } - $encryptedAuthCode = $this->getRequestParameter('code', $request); + // Get the Auth Code Payload from Repository + $ace = $this->authCodeRepository->getAuthCodeEntity($code); - if ($encryptedAuthCode === null) { - throw OAuthServerException::invalidRequest('code'); + if (empty($ace)) { + throw OAuthServerException::invalidRequest('code', 'Cannot validate the provided authorization code'); } + else { + // Get the Auth Code Payload from Repository + $ace = $this->authCodeRepository->getAuthCodeEntity($code); - try { - $authCodePayload = json_decode($this->decrypt($encryptedAuthCode)); + if (empty($ace)) { + throw OAuthServerException::invalidRequest('code', 'Cannot find authorization code'); + } + } - $this->validateAuthorizationCode($authCodePayload, $client, $request); + $this->validateAuthorizationCode($ace, $client, $request); - $scopes = $this->scopeRepository->finalizeScopes( - $this->validateScopes($authCodePayload->scopes), - $this->getIdentifier(), - $client, - $authCodePayload->user_id, - $authCodePayload->auth_code_id - ); - } catch (InvalidArgumentException $e) { - throw OAuthServerException::invalidGrant('Cannot validate the provided authorization code'); - } catch (LogicException $e) { - throw OAuthServerException::invalidRequest('code', 'Issue decrypting the authorization code', $e); - } + $scopes = $this->scopeRepository->finalizeScopes( + $ace->getScopes(), + $this->getIdentifier(), + $client, + $ace->getUser(), + $ace->getIdentifier() + ); $codeVerifier = $this->getRequestParameter('code_verifier', $request); // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack - if (!isset($authCodePayload->code_challenge) && $codeVerifier !== null) { + if ($ace->getCodeChallenge() === null && $codeVerifier !== null) { throw OAuthServerException::invalidRequest( 'code_challenge', 'code_verifier received when no code_challenge is present' ); } - if (isset($authCodePayload->code_challenge)) { - $this->validateCodeChallenge($authCodePayload, $codeVerifier); + if ($ace->getCodeChallenge() !== null) { + $this->validateCodeChallenge($ace, $codeVerifier); } // Issue and persist new access token - $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes); + $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $ace->getUser(), $scopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); @@ -158,12 +157,12 @@ public function respondToAccessTokenRequest( } // Revoke used auth code - $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id); + $this->authCodeRepository->revokeAuthCode($ace->getIdentifier()); return $responseType; } - private function validateCodeChallenge(object $authCodePayload, ?string $codeVerifier): void + private function validateCodeChallenge(AuthCodeEntityInterface $authCodeEntity, ?string $codeVerifier): void { if ($codeVerifier === null) { throw OAuthServerException::invalidRequest('code_verifier'); @@ -178,21 +177,20 @@ private function validateCodeChallenge(object $authCodePayload, ?string $codeVer ); } - if (property_exists($authCodePayload, 'code_challenge_method')) { - if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) { - $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method]; - - if (!isset($authCodePayload->code_challenge) || $codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) { - throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.'); - } - } else { - throw OAuthServerException::serverError( - sprintf( - 'Unsupported code challenge method `%s`', - $authCodePayload->code_challenge_method - ) - ); + + if (isset($this->codeChallengeVerifiers[$authCodeEntity->getCodeChallengeMethod()])) { + $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodeEntity->getCodeChallengeMethod()]; + + if ($authCodeEntity->getCodeChallenge() === null || $codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodeEntity->getCodeChallenge()) === false) { + throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.'); } + } else { + throw OAuthServerException::serverError( + sprintf( + 'Unsupported code challenge method `%s`', + $authCodeEntity->getCodeChallengeMethod() + ) + ); } } @@ -200,35 +198,41 @@ private function validateCodeChallenge(object $authCodePayload, ?string $codeVer * Validate the authorization code. */ private function validateAuthorizationCode( - stdClass $authCodePayload, + AuthCodeEntityInterface $authCodeEntity, ClientEntityInterface $client, ServerRequestInterface $request ): void { - if (!property_exists($authCodePayload, 'auth_code_id')) { + try { + if (empty($authCodeEntity->getIdentifier())) { + // Make sure its not empty + throw OAuthServerException::invalidRequest('code', 'Authorization code malformed'); + } + } catch (\Throwable $th) { + // $identifier must not be accessed before initialization throw OAuthServerException::invalidRequest('code', 'Authorization code malformed'); } - if (time() > $authCodePayload->expire_time) { + if (time() > $authCodeEntity->getExpiryDateTime()->getTimestamp()) { throw OAuthServerException::invalidGrant('Authorization code has expired'); } - if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { + if ($this->authCodeRepository->isAuthCodeRevoked($authCodeEntity->getIdentifier()) === true) { throw OAuthServerException::invalidGrant('Authorization code has been revoked'); } - if ($authCodePayload->client_id !== $client->getIdentifier()) { + if ($authCodeEntity->getClient()->getIdentifier() !== $client->getIdentifier()) { throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client'); } // The redirect URI is required in this request if it was specified // in the authorization request $redirectUri = $this->getRequestParameter('redirect_uri', $request); - if ($authCodePayload->redirect_uri !== null && $redirectUri === null) { + if ($authCodeEntity->getRedirectUri() !== null && $redirectUri === null) { throw OAuthServerException::invalidRequest('redirect_uri'); } // If a redirect URI has been provided ensure it matches the stored redirect URI - if ($redirectUri !== null && $authCodePayload->redirect_uri !== $redirectUri) { + if ($redirectUri !== null && $authCodeEntity->getRedirectUri() !== $redirectUri) { throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); } } @@ -363,34 +367,21 @@ public function completeAuthorizationRequest(AuthorizationRequestInterface $auth $authCode = $this->issueAuthCode( $this->authCodeTTL, $authorizationRequest->getClient(), - $authorizationRequest->getUser()->getIdentifier(), + $authorizationRequest->getUser(), $authorizationRequest->getRedirectUri(), - $authorizationRequest->getScopes() + $authorizationRequest->getScopes(), + $authorizationRequest->getCodeChallenge(), + $authorizationRequest->getCodeChallengeMethod() ); - $payload = [ - 'client_id' => $authCode->getClient()->getIdentifier(), - 'redirect_uri' => $authCode->getRedirectUri(), - 'auth_code_id' => $authCode->getIdentifier(), - 'scopes' => $authCode->getScopes(), - 'user_id' => $authCode->getUserIdentifier(), - 'expire_time' => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(), - 'code_challenge' => $authorizationRequest->getCodeChallenge(), - 'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(), - ]; - - $jsonPayload = json_encode($payload); - - if ($jsonPayload === false) { - throw new LogicException('An error was encountered when JSON encoding the authorization request response'); - } + $code = $authCode->getIdentifier(); $response = new RedirectResponse(); $response->setRedirectUri( $this->makeRedirectUri( $finalRedirectUri, [ - 'code' => $this->encrypt($jsonPayload), + 'code' => $code, 'state' => $authorizationRequest->getState(), ] ) diff --git a/src/Grant/ClientCredentialsGrant.php b/src/Grant/ClientCredentialsGrant.php index bee6abaa1..a24266c4c 100644 --- a/src/Grant/ClientCredentialsGrant.php +++ b/src/Grant/ClientCredentialsGrant.php @@ -34,9 +34,7 @@ public function respondToAccessTokenRequest( ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ): ResponseTypeInterface { - list($clientId) = $this->getClientCredentials($request); - - $client = $this->getClientEntityOrFail($clientId, $request); + $client = $this->validateClient($request); if (!$client->isConfidential()) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); @@ -44,9 +42,6 @@ public function respondToAccessTokenRequest( throw OAuthServerException::invalidClient($request); } - // Validate request - $this->validateClient($request); - $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); // Finalize the requested scopes diff --git a/src/Grant/DeviceCodeGrant.php b/src/Grant/DeviceCodeGrant.php index aa8cba0f5..43b4f5259 100644 --- a/src/Grant/DeviceCodeGrant.php +++ b/src/Grant/DeviceCodeGrant.php @@ -21,11 +21,14 @@ use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\DeviceCodeEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; @@ -113,7 +116,7 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ /** * {@inheritdoc} */ - public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void + public function completeDeviceAuthorizationRequest(string $deviceCode, UserEntityInterface $user, bool $userApproved): void { $deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode($deviceCode); @@ -121,11 +124,19 @@ public function completeDeviceAuthorizationRequest(string $deviceCode, string $u throw OAuthServerException::invalidRequest('device_code', 'Device code does not exist'); } - if ($userId === '') { + if ($user === null) { throw OAuthServerException::invalidRequest('user_id', 'User ID is required'); } - $deviceCode->setUserIdentifier($userId); + if (time() > $deviceCode->getExpiryDateTime()->getTimestamp()) { + throw OAuthServerException::expiredToken('device_code'); + } + + if ($this->deviceCodeRepository->isDeviceCodeRevoked($deviceCode->getIdentifier()) === true) { + throw OAuthServerException::invalidRequest('device_code', 'Device code has been revoked'); + } + + $deviceCode->setUser($user); $deviceCode->setUserApproved($userApproved); $this->deviceCodeRepository->persistDeviceCode($deviceCode); @@ -144,7 +155,7 @@ public function respondToAccessTokenRequest( $deviceCodeEntity = $this->validateDeviceCode($request, $client); // If device code has no user associated, respond with pending or slow down - if (is_null($deviceCodeEntity->getUserIdentifier())) { + if (is_null($deviceCodeEntity->getUser())) { $shouldSlowDown = $this->deviceCodePolledTooSoon($deviceCodeEntity->getLastPolledAt()); $deviceCodeEntity->setLastPolledAt(new DateTimeImmutable()); @@ -162,18 +173,18 @@ public function respondToAccessTokenRequest( } // Finalize the requested scopes - $finalizedScopes = $this->scopeRepository->finalizeScopes($deviceCodeEntity->getScopes(), $this->getIdentifier(), $client, $deviceCodeEntity->getUserIdentifier()); + $finalizedScopes = $this->scopeRepository->finalizeScopes($deviceCodeEntity->getScopes(), $this->getIdentifier(), $client, $deviceCodeEntity->getUser()); // Issue and persist new access token - $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $deviceCodeEntity->getUserIdentifier(), $finalizedScopes); - $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); + $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $deviceCodeEntity->getUser(), $finalizedScopes); + $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); if ($refreshToken !== null) { - $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); + $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); $responseType->setRefreshToken($refreshToken); } diff --git a/src/Grant/GrantTypeInterface.php b/src/Grant/GrantTypeInterface.php index 4e7dcf0f0..0d613b406 100644 --- a/src/Grant/GrantTypeInterface.php +++ b/src/Grant/GrantTypeInterface.php @@ -17,6 +17,7 @@ use DateInterval; use Defuse\Crypto\Key; use League\OAuth2\Server\CryptKeyInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\EventEmitting\EmitterAwareInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; @@ -98,7 +99,7 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ * * If the validation is successful a DeviceCode object is persisted. */ - public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void; + public function completeDeviceAuthorizationRequest(string $deviceCode, UserEntityInterface $user, bool $userApproved): void; /** * Set the client repository. @@ -120,13 +121,6 @@ public function setScopeRepository(ScopeRepositoryInterface $scopeRepository): v */ public function setDefaultScope(string $scope): void; - /** - * Set the path to the private key. - */ - public function setPrivateKey(CryptKeyInterface $privateKey): void; - - public function setEncryptionKey(Key|string|null $key = null): void; - /** * Enable or prevent the revocation of refresh tokens upon usage. */ diff --git a/src/Grant/ImplicitGrant.php b/src/Grant/ImplicitGrant.php index 81252dea1..8e4cc3b1a 100644 --- a/src/Grant/ImplicitGrant.php +++ b/src/Grant/ImplicitGrant.php @@ -162,16 +162,19 @@ public function completeAuthorizationRequest(AuthorizationRequestInterface $auth $authorizationRequest->getScopes(), $this->getIdentifier(), $authorizationRequest->getClient(), - $authorizationRequest->getUser()->getIdentifier() + $authorizationRequest->getUser() ); $accessToken = $this->issueAccessToken( $this->accessTokenTTL, $authorizationRequest->getClient(), - $authorizationRequest->getUser()->getIdentifier(), + $authorizationRequest->getUser(), $finalizedScopes ); + // TODO: next major release: this method needs `ServerRequestInterface` as an argument + // $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); + $response = new RedirectResponse(); $response->setRedirectUri( $this->makeRedirectUri( diff --git a/src/Grant/PasswordGrant.php b/src/Grant/PasswordGrant.php index f5d8da322..18b51a44e 100644 --- a/src/Grant/PasswordGrant.php +++ b/src/Grant/PasswordGrant.php @@ -58,11 +58,11 @@ public function respondToAccessTokenRequest( $scopes, $this->getIdentifier(), $client, - $user->getIdentifier() + $user ); // Issue and persist new access token - $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes); + $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user, $finalizedScopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); diff --git a/src/Grant/RefreshTokenGrant.php b/src/Grant/RefreshTokenGrant.php index 34e3f20b4..d9355c6e4 100644 --- a/src/Grant/RefreshTokenGrant.php +++ b/src/Grant/RefreshTokenGrant.php @@ -15,7 +15,9 @@ namespace League\OAuth2\Server\Grant; use DateInterval; +use DateTimeImmutable; use Exception; +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; @@ -53,18 +55,29 @@ public function respondToAccessTokenRequest( $client = $this->validateClient($request); $oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier()); + if ($oldRefreshToken == null) { + throw OAuthServerException::invalidRefreshToken('Refresh token cannot be found'); + } + + $originalScopes = $oldRefreshToken->getScopes(); + $originalScopeArray = []; + foreach ($originalScopes as $scopeEntity) { + $originalScopeArray[$scopeEntity->getIdentifier()] = $scopeEntity->getIdentifier(); + } + $originalScopeArray = array_values($originalScopeArray); + $scopes = $this->validateScopes( $this->getRequestParameter( 'scope', $request, - implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes']) + implode(self::SCOPE_DELIMITER_STRING, $originalScopeArray) ) ); // The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure // the request doesn't include any new scopes foreach ($scopes as $scope) { - if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes'], true) === false) { + if (in_array($scope->getIdentifier(), $originalScopeArray, true) === false) { throw OAuthServerException::invalidScope($scope->getIdentifier()); } } @@ -72,17 +85,13 @@ public function respondToAccessTokenRequest( $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client); // Expire old tokens - $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); + $this->accessTokenRepository->revokeAccessToken($oldRefreshToken->getAccessToken()->getIdentifier()); if ($this->revokeRefreshTokens) { - $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); + $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken->getIdentifier()); } // Issue and persist new access token - $userId = $oldRefreshToken['user_id']; - if (is_int($userId)) { - $userId = (string) $userId; - } - $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $userId, $scopes); + $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken->getUser(), $scopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); @@ -100,35 +109,34 @@ public function respondToAccessTokenRequest( /** * @throws OAuthServerException * - * @return array + * @return RefreshTokenEntityInterface */ - protected function validateOldRefreshToken(ServerRequestInterface $request, string $clientId): array + protected function validateOldRefreshToken(ServerRequestInterface $request, string $clientId): ?RefreshTokenEntityInterface { - $encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request) + $refreshTokenParam = $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); + $refreshTokenEntity = $this->refreshTokenRepository->getRefreshTokenEntity($refreshTokenParam); + + if ($refreshTokenEntity === null) { + throw OAuthServerException::invalidRefreshToken('Cannot find refresh token'); } - $refreshTokenData = json_decode($refreshToken, true); - if ($refreshTokenData['client_id'] !== $clientId) { + if ($refreshTokenEntity->getClient()->getIdentifier() !== $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()) { + if ($refreshTokenEntity->getExpiryDateTime()->getTimestamp() < time()) { throw OAuthServerException::invalidRefreshToken('Token has expired'); } - if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { + if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenEntity->getIdentifier()) === true) { throw OAuthServerException::invalidRefreshToken('Token has been revoked'); } - return $refreshTokenData; + return $refreshTokenEntity; } /** diff --git a/src/Repositories/AccessTokenRepositoryInterface.php b/src/Repositories/AccessTokenRepositoryInterface.php index 8bac8be64..30e60d3b1 100644 --- a/src/Repositories/AccessTokenRepositoryInterface.php +++ b/src/Repositories/AccessTokenRepositoryInterface.php @@ -12,9 +12,11 @@ namespace League\OAuth2\Server\Repositories; +use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; /** @@ -30,7 +32,7 @@ interface AccessTokenRepositoryInterface extends RepositoryInterface public function getNewToken( ClientEntityInterface $clientEntity, array $scopes, - string|null $userIdentifier = null + ?UserEntityInterface $user ): AccessTokenEntityInterface; /** @@ -41,4 +43,6 @@ public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEnt public function revokeAccessToken(string $tokenId): void; public function isAccessTokenRevoked(string $tokenId): bool; + + public function getAccessTokenEntity(string $tokenId): ?AccessTokenEntityInterface; } diff --git a/src/Repositories/AuthCodeRepositoryInterface.php b/src/Repositories/AuthCodeRepositoryInterface.php index 89ff86b87..fdc335e0f 100644 --- a/src/Repositories/AuthCodeRepositoryInterface.php +++ b/src/Repositories/AuthCodeRepositoryInterface.php @@ -30,4 +30,11 @@ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): voi public function revokeAuthCode(string $codeId): void; public function isAuthCodeRevoked(string $codeId): bool; + + /** + * Get Auth code entity from repository + * + * @return ?AuthCodeEntityInterface + */ + public function getAuthCodeEntity(string $codeId): ?AuthCodeEntityInterface; } diff --git a/src/Repositories/DeviceCodeRepositoryInterface.php b/src/Repositories/DeviceCodeRepositoryInterface.php index 09575ab18..d61bf3a9f 100644 --- a/src/Repositories/DeviceCodeRepositoryInterface.php +++ b/src/Repositories/DeviceCodeRepositoryInterface.php @@ -33,7 +33,14 @@ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity): * Get a device code entity. */ public function getDeviceCodeEntityByDeviceCode( - string $deviceCodeEntity + string $deviceCodeEntity // TODO: next major release: rename to `$deviceCode` + ): ?DeviceCodeEntityInterface; + + /** + * Get a device code entity. + */ + public function getDeviceCodeEntityByUserCode( + string $userCode ): ?DeviceCodeEntityInterface; /** diff --git a/src/Repositories/RefreshTokenRepositoryInterface.php b/src/Repositories/RefreshTokenRepositoryInterface.php index a25e50133..9c484a1bf 100644 --- a/src/Repositories/RefreshTokenRepositoryInterface.php +++ b/src/Repositories/RefreshTokenRepositoryInterface.php @@ -30,4 +30,11 @@ public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshToken public function revokeRefreshToken(string $tokenId): void; public function isRefreshTokenRevoked(string $tokenId): bool; + + /** + * Get Refresh Token entity from repository + * + * @return ?RefreshTokenEntityInterface + */ + public function getRefreshTokenEntity(string $tokenId): ?RefreshTokenEntityInterface; } diff --git a/src/Repositories/ScopeRepositoryInterface.php b/src/Repositories/ScopeRepositoryInterface.php index e5ae7c716..2242074c2 100644 --- a/src/Repositories/ScopeRepositoryInterface.php +++ b/src/Repositories/ScopeRepositoryInterface.php @@ -14,6 +14,7 @@ use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; /** * Scope interface. @@ -39,7 +40,7 @@ public function finalizeScopes( array $scopes, string $grantType, ClientEntityInterface $clientEntity, - string|null $userIdentifier = null, + ?UserEntityInterface $user = null, ?string $authCodeId = null ): array; } diff --git a/src/ResponseTypes/AbstractResponseType.php b/src/ResponseTypes/AbstractResponseType.php index 2af00e224..b3ce204d4 100644 --- a/src/ResponseTypes/AbstractResponseType.php +++ b/src/ResponseTypes/AbstractResponseType.php @@ -15,20 +15,15 @@ namespace League\OAuth2\Server\ResponseTypes; use League\OAuth2\Server\CryptKeyInterface; -use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; abstract class AbstractResponseType implements ResponseTypeInterface { - use CryptTrait; - protected AccessTokenEntityInterface $accessToken; protected RefreshTokenEntityInterface $refreshToken; - protected CryptKeyInterface $privateKey; - public function setAccessToken(AccessTokenEntityInterface $accessToken): void { $this->accessToken = $accessToken; @@ -38,9 +33,4 @@ public function setRefreshToken(RefreshTokenEntityInterface $refreshToken): void { $this->refreshToken = $refreshToken; } - - public function setPrivateKey(CryptKeyInterface $key): void - { - $this->privateKey = $key; - } } diff --git a/src/ResponseTypes/BearerTokenResponse.php b/src/ResponseTypes/BearerTokenResponse.php index dd49b99ba..004e0322b 100644 --- a/src/ResponseTypes/BearerTokenResponse.php +++ b/src/ResponseTypes/BearerTokenResponse.php @@ -35,20 +35,7 @@ public function generateHttpResponse(ResponseInterface $response): ResponseInter ]; if (isset($this->refreshToken)) { - $refreshTokenPayload = json_encode([ - 'client_id' => $this->accessToken->getClient()->getIdentifier(), - 'refresh_token_id' => $this->refreshToken->getIdentifier(), - 'access_token_id' => $this->accessToken->getIdentifier(), - 'scopes' => $this->accessToken->getScopes(), - 'user_id' => $this->accessToken->getUserIdentifier(), - 'expire_time' => $this->refreshToken->getExpiryDateTime()->getTimestamp(), - ]); - - if ($refreshTokenPayload === false) { - throw new LogicException('Error encountered JSON encoding the refresh token payload'); - } - - $responseParams['refresh_token'] = $this->encrypt($refreshTokenPayload); + $responseParams['refresh_token'] = $this->refreshToken->getIdentifier(); } $responseParams = json_encode(array_merge($this->getExtraParams($this->accessToken), $responseParams)); diff --git a/src/ResponseTypes/ResponseTypeInterface.php b/src/ResponseTypes/ResponseTypeInterface.php index 8e70ae9f8..66c9d355c 100644 --- a/src/ResponseTypes/ResponseTypeInterface.php +++ b/src/ResponseTypes/ResponseTypeInterface.php @@ -26,6 +26,4 @@ public function setAccessToken(AccessTokenEntityInterface $accessToken): void; public function setRefreshToken(RefreshTokenEntityInterface $refreshToken): void; public function generateHttpResponse(ResponseInterface $response): ResponseInterface; - - public function setEncryptionKey(Key|string|null $key = null): void; } diff --git a/tests/AuthorizationServerTest.php b/tests/AuthorizationServerTest.php index 72add207b..3015a5845 100644 --- a/tests/AuthorizationServerTest.php +++ b/tests/AuthorizationServerTest.php @@ -56,8 +56,6 @@ public function testGrantTypeGetsEnabled(): void $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(), $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), - 'file://' . __DIR__ . '/Stubs/private.key', - base64_encode(random_bytes(36)), new StubResponseType() ); @@ -73,8 +71,6 @@ public function testRespondToRequestInvalidGrantType(): void $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(), $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), - 'file://' . __DIR__ . '/Stubs/private.key', - base64_encode(random_bytes(36)), new StubResponseType() ); @@ -111,8 +107,6 @@ public function testRespondToRequest(): void $clientRepository, $accessTokenRepositoryMock, $scopeRepositoryMock, - 'file://' . __DIR__ . '/Stubs/private.key', - base64_encode(random_bytes(36)), new StubResponseType() ); @@ -134,8 +128,6 @@ public function testGetResponseType(): void $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), - 'file://' . __DIR__ . '/Stubs/private.key', - 'file://' . __DIR__ . '/Stubs/public.key' ); $abstractGrantReflection = new ReflectionClass($server); @@ -145,57 +137,10 @@ public function testGetResponseType(): void self::assertInstanceOf(BearerTokenResponse::class, $method->invoke($server)); } - public function testGetResponseTypeExtended(): void - { - $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - $privateKey = 'file://' . __DIR__ . '/Stubs/private.key'; - $encryptionKey = 'file://' . __DIR__ . '/Stubs/public.key'; - - $server = new AuthorizationServer( - $clientRepository, - $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), - 'file://' . __DIR__ . '/Stubs/private.key', - 'file://' . __DIR__ . '/Stubs/public.key' - ); - - $abstractGrantReflection = new ReflectionClass($server); - $method = $abstractGrantReflection->getMethod('getResponseType'); - $method->setAccessible(true); - - $responseType = $method->invoke($server); - - $responseTypeReflection = new ReflectionClass($responseType); - - $privateKeyProperty = $responseTypeReflection->getProperty('privateKey'); - $privateKeyProperty->setAccessible(true); - - $encryptionKeyProperty = $responseTypeReflection->getProperty('encryptionKey'); - $encryptionKeyProperty->setAccessible(true); - - // generated instances should have keys setup - self::assertSame($privateKey, $privateKeyProperty->getValue($responseType)->getKeyPath()); - self::assertSame($encryptionKey, $encryptionKeyProperty->getValue($responseType)); - } - public function testMultipleRequestsGetDifferentResponseTypeInstances(): void { - $privateKey = 'file://' . __DIR__ . '/Stubs/private.key'; - $encryptionKey = 'file://' . __DIR__ . '/Stubs/public.key'; - $responseTypePrototype = new class() extends BearerTokenResponse { - protected CryptKeyInterface $privateKey; - protected Key|string|null $encryptionKey = null; - - public function getPrivateKey(): CryptKeyInterface - { - return $this->privateKey; - } - - public function getEncryptionKey(): Key|string|null - { - return $this->encryptionKey; - } + }; $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -204,8 +149,6 @@ public function getEncryptionKey(): Key|string|null $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), - $privateKey, - $encryptionKey, $responseTypePrototype ); @@ -216,10 +159,6 @@ public function getEncryptionKey(): Key|string|null $responseTypeA = $method->invoke($server); $responseTypeB = $method->invoke($server); - // generated instances should have keys setup - self::assertSame($privateKey, $responseTypeA->getPrivateKey()->getKeyPath()); - self::assertSame($encryptionKey, $responseTypeA->getEncryptionKey()); - // all instances should be different but based on the same prototype self::assertSame(get_class($responseTypePrototype), get_class($responseTypeA)); self::assertSame(get_class($responseTypePrototype), get_class($responseTypeB)); @@ -235,9 +174,7 @@ public function testCompleteAuthorizationRequest(): void $server = new AuthorizationServer( $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), - 'file://' . __DIR__ . '/Stubs/private.key', - 'file://' . __DIR__ . '/Stubs/public.key' + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock() ); $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); @@ -292,9 +229,7 @@ public function testValidateAuthorizationRequest(): void $server = new AuthorizationServer( $clientRepositoryMock, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - $scopeRepositoryMock, - 'file://' . __DIR__ . '/Stubs/private.key', - 'file://' . __DIR__ . '/Stubs/public.key' + $scopeRepositoryMock ); $server->setDefaultScope(self::DEFAULT_SCOPE); @@ -322,9 +257,7 @@ public function testValidateAuthorizationRequestUnregistered(): void $server = new AuthorizationServer( $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(), $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), - $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), - 'file://' . __DIR__ . '/Stubs/private.key', - 'file://' . __DIR__ . '/Stubs/public.key' + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock() ); $request = (new ServerRequest())->withQueryParams([ diff --git a/tests/Grant/AbstractGrantTest.php b/tests/Grant/AbstractGrantTest.php index adfb880be..6cd8d2f15 100644 --- a/tests/Grant/AbstractGrantTest.php +++ b/tests/Grant/AbstractGrantTest.php @@ -23,6 +23,7 @@ use LeagueTests\Stubs\ClientEntity; use LeagueTests\Stubs\RefreshTokenEntity; use LeagueTests\Stubs\ScopeEntity; +use LeagueTests\Stubs\UserEntity; use LogicException; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -265,84 +266,6 @@ public function testValidateClientInvalidClientSecret(): void $validateClientMethod->invoke($grantMock, $serverRequest, true, true); } - public function testValidateClientInvalidRedirectUri(): void - { - $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); - $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - $clientRepositoryMock->method('getClientEntity')->willReturn($client); - - /** @var AbstractGrant $grantMock */ - $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); - $grantMock->setClientRepository($clientRepositoryMock); - - $abstractGrantReflection = new ReflectionClass($grantMock); - - $serverRequest = (new ServerRequest())->withParsedBody([ - 'client_id' => 'foo', - 'redirect_uri' => 'http://bar/foo', - ]); - - $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); - $validateClientMethod->setAccessible(true); - - $this->expectException(OAuthServerException::class); - - $validateClientMethod->invoke($grantMock, $serverRequest, true, true); - } - - public function testValidateClientInvalidRedirectUriArray(): void - { - $client = new ClientEntity(); - $client->setRedirectUri(['http://foo/bar']); - $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - $clientRepositoryMock->method('getClientEntity')->willReturn($client); - - /** @var AbstractGrant $grantMock */ - $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); - $grantMock->setClientRepository($clientRepositoryMock); - - $abstractGrantReflection = new ReflectionClass($grantMock); - - $serverRequest = (new ServerRequest())->withParsedBody([ - 'client_id' => 'foo', - 'redirect_uri' => 'http://bar/foo', - ]); - - $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); - $validateClientMethod->setAccessible(true); - - $this->expectException(OAuthServerException::class); - - $validateClientMethod->invoke($grantMock, $serverRequest, true, true); - } - - public function testValidateClientMalformedRedirectUri(): void - { - $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); - $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - $clientRepositoryMock->method('getClientEntity')->willReturn($client); - - /** @var AbstractGrant $grantMock */ - $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); - $grantMock->setClientRepository($clientRepositoryMock); - - $abstractGrantReflection = new ReflectionClass($grantMock); - - $serverRequest = (new ServerRequest())->withParsedBody([ - 'client_id' => 'foo', - 'redirect_uri' => ['not', 'a', 'string'], - ]); - - $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); - $validateClientMethod->setAccessible(true); - - $this->expectException(OAuthServerException::class); - - $validateClientMethod->invoke($grantMock, $serverRequest, true, true); - } - public function testValidateClientBadClient(): void { $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -398,6 +321,7 @@ public function testIssueRefreshToken(): void $issueRefreshTokenMethod->setAccessible(true); $accessToken = new AccessTokenEntity(); + $accessToken->setClient(new ClientEntity()); /** @var RefreshTokenEntityInterface $refreshToken */ $refreshToken = $issueRefreshTokenMethod->invoke($grantMock, $accessToken); @@ -423,6 +347,34 @@ public function testIssueNullRefreshToken(): void $issueRefreshTokenMethod->setAccessible(true); $accessToken = new AccessTokenEntity(); + $accessToken->setClient(new ClientEntity()); + self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken)); + } + + public function testIssueNullRefreshTokenUnauthorizedClient(): void + { + $client = $this->getMockBuilder(ClientEntity::class)->getMock(); + $client + ->expects(self::once()) + ->method('supportsGrantType') + ->with('refresh_token') + ->willReturn(false); + + $refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepoMock->expects(self::never())->method('getNewRefreshToken'); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setRefreshTokenTTL(new DateInterval('PT1M')); + $grantMock->setRefreshTokenRepository($refreshTokenRepoMock); + + $abstractGrantReflection = new ReflectionClass($grantMock); + $issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken'); + $issueRefreshTokenMethod->setAccessible(true); + + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken)); } @@ -433,19 +385,21 @@ public function testIssueAccessToken(): void /** @var AbstractGrant $grantMock */ $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); - $grantMock->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grantMock->setAccessTokenRepository($accessTokenRepoMock); $abstractGrantReflection = new ReflectionClass($grantMock); $issueAccessTokenMethod = $abstractGrantReflection->getMethod('issueAccessToken'); $issueAccessTokenMethod->setAccessible(true); + $user = new UserEntity(); + $user->setIdentifier('123'); + /** @var AccessTokenEntityInterface $accessToken */ $accessToken = $issueAccessTokenMethod->invoke( $grantMock, new DateInterval('PT1H'), new ClientEntity(), - 123, + $user, [new ScopeEntity()] ); @@ -468,13 +422,16 @@ public function testIssueAuthCode(): void $scope = new ScopeEntity(); $scope->setIdentifier('scopeId'); + $user = new UserEntity(); + $user->setIdentifier('123'); + self::assertInstanceOf( AuthCodeEntityInterface::class, $issueAuthCodeMethod->invoke( $grantMock, new DateInterval('PT1H'), new ClientEntity(), - 123, + $user, 'http://foo/bar', [$scope] ) @@ -576,4 +533,30 @@ public function testCompleteAuthorizationRequest(): void $grantMock->completeAuthorizationRequest(new AuthorizationRequest()); } + + public function testUnauthorizedClient(): void + { + $client = $this->getMockBuilder(ClientEntity::class)->getMock(); + $client->method('supportsGrantType')->willReturn(false); + + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock + ->expects(self::once()) + ->method('getClientEntity') + ->with('foo') + ->willReturn($client); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setClientRepository($clientRepositoryMock); + + $abstractGrantReflection = new ReflectionClass($grantMock); + + $getClientEntityOrFailMethod = $abstractGrantReflection->getMethod('getClientEntityOrFail'); + $getClientEntityOrFailMethod->setAccessible(true); + + $this->expectException(OAuthServerException::class); + + $getClientEntityOrFailMethod->invoke($grantMock, 'foo', new ServerRequest()); + } } diff --git a/tests/Grant/AuthCodeGrantTest.php b/tests/Grant/AuthCodeGrantTest.php index 390001721..8029b7398 100644 --- a/tests/Grant/AuthCodeGrantTest.php +++ b/tests/Grant/AuthCodeGrantTest.php @@ -4,6 +4,7 @@ namespace LeagueTests\Grant; +use DateTimeImmutable; use DateInterval; use Laminas\Diactoros\Response; use Laminas\Diactoros\ServerRequest; @@ -17,12 +18,14 @@ use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use League\OAuth2\Server\RequestAccessTokenEvent; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\RedirectResponse; use LeagueTests\Stubs\AccessTokenEntity; use LeagueTests\Stubs\AuthCodeEntity; use LeagueTests\Stubs\ClientEntity; -use LeagueTests\Stubs\CryptTraitStub; use LeagueTests\Stubs\RefreshTokenEntity; use LeagueTests\Stubs\ScopeEntity; use LeagueTests\Stubs\StubResponseType; @@ -40,15 +43,13 @@ class AuthCodeGrantTest extends TestCase private const DEFAULT_SCOPE = 'basic'; private const REDIRECT_URI = 'https://foo/bar'; - protected CryptTraitStub $cryptStub; - private const CODE_VERIFIER = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; private const CODE_CHALLENGE = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'; public function setUp(): void { - $this->cryptStub = new CryptTraitStub(); + } public function testGetIdentifier(): void @@ -532,7 +533,6 @@ public function testCompleteAuthorizationRequest(): void $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); - $grant->setEncryptionKey($this->cryptStub->getKey()); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } @@ -557,7 +557,6 @@ public function testCompleteAuthorizationRequestWithMultipleRedirectUrisOnClient $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); - $grant->setEncryptionKey($this->cryptStub->getKey()); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } @@ -582,7 +581,6 @@ public function testCompleteAuthorizationRequestDenied(): void $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); - $grant->setEncryptionKey($this->cryptStub->getKey()); try { $grant->completeAuthorizationRequest($authRequest); @@ -616,15 +614,32 @@ public function testRespondToAccessTokenRequest(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -632,8 +647,27 @@ public function testRespondToAccessTokenRequest(): void $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $accessTokenEventEmitted = false; + $refreshTokenEventEmitted = false; + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::ACCESS_TOKEN_ISSUED, + function ($event) use (&$accessTokenEventEmitted): void { + self::assertInstanceOf(RequestAccessTokenEvent::class, $event); + + $accessTokenEventEmitted = true; + } + ); + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::REFRESH_TOKEN_ISSUED, + function ($event) use (&$refreshTokenEventEmitted): void { + self::assertInstanceOf(RequestRefreshTokenEvent::class, $event); + + $refreshTokenEventEmitted = true; + } + ); $request = new ServerRequest( [], @@ -647,17 +681,9 @@ public function testRespondToAccessTokenRequest(): void [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', + 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -665,6 +691,14 @@ public function testRespondToAccessTokenRequest(): void $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); + + if (!$accessTokenEventEmitted) { + self::fail('Access token issued event is not emitted.'); + } + + if (!$refreshTokenEventEmitted) { + self::fail('Refresh token issued event is not emitted.'); + } } public function testRespondToAccessTokenRequestWithDefaultRedirectUri(): void @@ -685,16 +719,34 @@ public function testRespondToAccessTokenRequestWithDefaultRedirectUri(): void $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(null); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -702,8 +754,6 @@ public function testRespondToAccessTokenRequestWithDefaultRedirectUri(): void $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -717,16 +767,8 @@ public function testRespondToAccessTokenRequestWithDefaultRedirectUri(): void [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => null, - ], JSON_THROW_ON_ERROR) - ), + 'client_secret' => 'bar', + 'code' => $ace->getIdentifier() ] ); @@ -743,19 +785,37 @@ public function testRespondToAccessTokenRequestUsingHttpBasicAuth(): void $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $authCodeGrant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $refreshTokenRepositoryMock, new DateInterval('PT10M') ); @@ -763,8 +823,6 @@ public function testRespondToAccessTokenRequestUsingHttpBasicAuth(): void $authCodeGrant->setClientRepository($clientRepositoryMock); $authCodeGrant->setScopeRepository($scopeRepositoryMock); $authCodeGrant->setAccessTokenRepository($accessTokenRepositoryMock); - $authCodeGrant->setEncryptionKey($this->cryptStub->getKey()); - $authCodeGrant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -780,16 +838,7 @@ public function testRespondToAccessTokenRequestUsingHttpBasicAuth(): void [ 'grant_type' => 'authorization_code', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'client_id' => 'foo', - 'expire_time' => time() + 3600, - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -806,6 +855,7 @@ public function testRespondToAccessTokenRequestForPublicClient(): void $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -813,15 +863,32 @@ public function testRespondToAccessTokenRequestForPublicClient(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -829,8 +896,6 @@ public function testRespondToAccessTokenRequestForPublicClient(): void $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -845,16 +910,7 @@ public function testRespondToAccessTokenRequestForPublicClient(): void 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -871,22 +927,41 @@ public function testRespondToAccessTokenRequestNullRefreshToken(): void $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $refreshTokenRepositoryMock, new DateInterval('PT10M') ); @@ -894,8 +969,6 @@ public function testRespondToAccessTokenRequestNullRefreshToken(): void $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -910,16 +983,7 @@ public function testRespondToAccessTokenRequestNullRefreshToken(): void 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -948,16 +1012,35 @@ public function testRespondToAccessTokenRequestCodeChallengePlain(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + $ace->setCodeChallenge(self::CODE_VERIFIER); + $ace->setCodeChallengeMethod('plain'); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), - $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + $authCodeRepository, + $refreshTokenRepositoryMock, new DateInterval('PT10M') ); @@ -965,8 +1048,6 @@ public function testRespondToAccessTokenRequestCodeChallengePlain(): void $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -980,20 +1061,10 @@ public function testRespondToAccessTokenRequestCodeChallengePlain(): void [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', + 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - 'code_challenge' => self::CODE_VERIFIER, - 'code_challenge_method' => 'plain', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1022,15 +1093,34 @@ public function testRespondToAccessTokenRequestCodeChallengeS256(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + $ace->setCodeChallenge(self::CODE_CHALLENGE); + $ace->setCodeChallengeMethod('S256'); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -1039,8 +1129,6 @@ public function testRespondToAccessTokenRequestCodeChallengeS256(): void $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -1054,20 +1142,10 @@ public function testRespondToAccessTokenRequestCodeChallengeS256(): void [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', + 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - 'code_challenge' => self::CODE_CHALLENGE, - 'code_challenge_method' => 'S256', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1100,8 +1178,23 @@ public function testPKCEDowngradeBlocked(): void $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(null); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -1110,8 +1203,6 @@ public function testPKCEDowngradeBlocked(): void $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -1127,19 +1218,7 @@ public function testPKCEDowngradeBlocked(): void 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, - 'code' => $this->cryptStub->doEncrypt( - json_encode( - [ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - ], - JSON_THROW_ON_ERROR - ) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1163,13 +1242,27 @@ public function testRespondToAccessTokenRequestMissingRedirectUri(): void $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri('http://foo/bar'); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1183,14 +1276,7 @@ public function testRespondToAccessTokenRequestMissingRedirectUri(): void [ 'client_id' => 'foo', 'grant_type' => 'authorization_code', - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1213,13 +1299,26 @@ public function testRespondToAccessTokenRequestRedirectUriMismatch(): void $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri('http://foo/bar'); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1234,14 +1333,7 @@ public function testRespondToAccessTokenRequestRedirectUriMismatch(): void 'client_id' => 'foo', 'grant_type' => 'authorization_code', 'redirect_uri' => 'http://bar/foo', - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1264,13 +1356,27 @@ public function testRejectAccessTokenRequestIfRedirectUriSpecifiedButNotInOrigin $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(null); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1285,14 +1391,7 @@ public function testRejectAccessTokenRequestIfRedirectUriSpecifiedButNotInOrigin 'client_id' => 'foo', 'grant_type' => 'authorization_code', 'redirect_uri' => 'http://bar/foo', - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'redirect_uri' => null, - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1325,7 +1424,6 @@ public function testRespondToAccessTokenRequestMissingCode(): void $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1351,57 +1449,6 @@ public function testRespondToAccessTokenRequestMissingCode(): void $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } - public function testRespondToAccessTokenRequestWithRefreshTokenInsteadOfAuthCode(): void - { - $client = new ClientEntity(); - $client->setRedirectUri(self::REDIRECT_URI); - - $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - $clientRepositoryMock->method('getClientEntity')->willReturn($client); - - $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), - $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), - new DateInterval('PT10M') - ); - - $grant->setClientRepository($clientRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - - $request = new ServerRequest( - [], - [], - null, - 'POST', - 'php://input', - [], - [], - [], - [ - 'grant_type' => 'authorization_code', - 'client_id' => 'foo', - 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => 123, - 'expire_time' => time() + 3600, - ], JSON_THROW_ON_ERROR) - ), - ] - ); - - try { - /* @var StubResponseType $response */ - $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); - } catch (OAuthServerException $e) { - self::assertEquals('Authorization code malformed', $e->getHint()); - } - } - public function testRespondToAccessTokenRequestWithAuthCodeNotAString(): void { $client = new ClientEntity(); @@ -1417,7 +1464,6 @@ public function testRespondToAccessTokenRequestWithAuthCodeNotAString(): void ); $grant->setClientRepository($clientRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1447,15 +1493,34 @@ public function testRespondToAccessTokenRequestExpiredCode(): void $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setScopeRepository($scopeRepositoryMock); $request = new ServerRequest( [], @@ -1470,16 +1535,7 @@ public function testRespondToAccessTokenRequestExpiredCode(): void 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() - 3600, - 'client_id' => 'foo', - 'user_id' => 123, - 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1512,7 +1568,23 @@ public function testRespondToAccessTokenRequestRevokedCode(): void $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepositoryMock->method('isAuthCodeRevoked')->willReturn(true); + $authCodeRepositoryMock->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepositoryMock->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), @@ -1521,7 +1593,7 @@ public function testRespondToAccessTokenRequestRevokedCode(): void $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setScopeRepository($scopeRepositoryMock); $request = new ServerRequest( [], @@ -1535,17 +1607,9 @@ public function testRespondToAccessTokenRequestRevokedCode(): void [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', + 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => 123, - 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1566,9 +1630,21 @@ public function testRespondToAccessTokenRequestClientMismatch(): void $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); + $client2 = new ClientEntity(); + + $client2->setIdentifier('foo'); + $client2->setRedirectUri(self::REDIRECT_URI); + $client2->setConfidential(); + + $client3 = new ClientEntity(); + + $client3->setIdentifier('bar'); + $client3->setRedirectUri(self::REDIRECT_URI); + $client3->setConfidential(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('getClientEntity')->willReturn($client, $client2, $client3); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); @@ -1577,15 +1653,33 @@ public function testRespondToAccessTokenRequestClientMismatch(): void $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client3); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setScopeRepository($scopeRepositoryMock); $request = new ServerRequest( [], @@ -1599,17 +1693,9 @@ public function testRespondToAccessTokenRequestClientMismatch(): void [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', + 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'bar', - 'user_id' => 123, - 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1640,15 +1726,18 @@ public function testRespondToAccessTokenRequestBadCode(): void $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getAuthCodeEntity')->willReturn(null); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1662,72 +1751,18 @@ public function testRespondToAccessTokenRequestBadCode(): void [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', + 'client_secret' => 'bar', 'redirect_uri' => self::REDIRECT_URI, 'code' => 'badCode', ] ); - try { - /* @var StubResponseType $response */ - $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); - } catch (OAuthServerException $e) { - self::assertEquals($e->getErrorType(), 'invalid_grant'); - self::assertEquals($e->getHint(), 'Cannot validate the provided authorization code'); - } - } - - public function testRespondToAccessTokenRequestNoEncryptionKey(): void - { - $client = new ClientEntity(); - - $client->setIdentifier('foo'); - $client->setRedirectUri(self::REDIRECT_URI); - $client->setConfidential(); - - $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - - $clientRepositoryMock->method('getClientEntity')->willReturn($client); - $clientRepositoryMock->method('validateClient')->willReturn(true); - - $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); - - $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); - $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); - - $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), - $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), - new DateInterval('PT10M') - ); - $grant->setClientRepository($clientRepositoryMock); - $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - // We deliberately don't set an encryption key here - - $request = new ServerRequest( - [], - [], - null, - 'POST', - 'php://input', - [], - [], - [], - [ - 'grant_type' => 'authorization_code', - 'client_id' => 'foo', - 'redirect_uri' => self::REDIRECT_URI, - 'code' => 'badCode', - ] - ); - try { /* @var StubResponseType $response */ $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); } catch (OAuthServerException $e) { self::assertEquals($e->getErrorType(), 'invalid_request'); - self::assertEquals($e->getHint(), 'Issue decrypting the authorization code'); + self::assertEquals($e->getHint(), 'Cannot validate the provided authorization code'); } } @@ -1737,7 +1772,6 @@ public function testRespondToAccessTokenRequestBadCodeVerifierPlain(): void $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); - $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -1757,8 +1791,25 @@ public function testRespondToAccessTokenRequestBadCodeVerifierPlain(): void $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepositoryMock->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + $ace->setCodeChallenge('foobar'); + $ace->setCodeChallengeMethod('plain'); + + $authCodeRepositoryMock->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -1767,7 +1818,6 @@ public function testRespondToAccessTokenRequestBadCodeVerifierPlain(): void $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1783,18 +1833,7 @@ public function testRespondToAccessTokenRequestBadCodeVerifierPlain(): void 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - 'code_challenge' => 'foobar', - 'code_challenge_method' => 'plain', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1812,7 +1851,6 @@ public function testRespondToAccessTokenRequestBadCodeVerifierS256(): void $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); - $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -1832,8 +1870,25 @@ public function testRespondToAccessTokenRequestBadCodeVerifierS256(): void $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepositoryMock->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + $ace->setCodeChallenge('foobar'); + $ace->setCodeChallengeMethod('S256'); + + $authCodeRepositoryMock->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -1842,7 +1897,6 @@ public function testRespondToAccessTokenRequestBadCodeVerifierS256(): void $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1858,18 +1912,7 @@ public function testRespondToAccessTokenRequestBadCodeVerifierS256(): void 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'nope', - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - 'code_challenge' => 'foobar', - 'code_challenge_method' => 'S256', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1887,7 +1930,6 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); - $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -1907,8 +1949,25 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepositoryMock->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + $ace->setCodeChallenge(self::CODE_CHALLENGE); + $ace->setCodeChallengeMethod('S256'); + + $authCodeRepositoryMock->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -1917,7 +1976,6 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -1933,18 +1991,7 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'dqX7C-RbqjHYtytmhGTigKdZCXfxq-+xbsk9_GxUcaE', // Malformed code. Contains `+`. - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - 'code_challenge' => self::CODE_CHALLENGE, - 'code_challenge_method' => 'S256', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -1962,7 +2009,6 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); - $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -1982,8 +2028,25 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepositoryMock->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + $ace->setCodeChallenge(self::CODE_CHALLENGE); + $ace->setCodeChallengeMethod('S256'); + + $authCodeRepositoryMock->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -1992,7 +2055,6 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -2008,18 +2070,7 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'dqX7C-RbqjHY', // Malformed code. Invalid length. - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - 'code_challenge' => 'R7T1y1HPNFvs1WDCrx4lfoBS6KD2c71pr8OHvULjvv8', - 'code_challenge_method' => 'S256', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -2037,7 +2088,6 @@ public function testRespondToAccessTokenRequestMissingCodeVerifier(): void $client->setIdentifier('foo'); $client->setRedirectUri(self::REDIRECT_URI); - $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -2057,8 +2107,25 @@ public function testRespondToAccessTokenRequestMissingCodeVerifier(): void $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepositoryMock->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + $ace->setCodeChallenge('foobar'); + $ace->setCodeChallengeMethod('plain'); + + $authCodeRepositoryMock->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -2067,7 +2134,6 @@ public function testRespondToAccessTokenRequestMissingCodeVerifier(): void $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); $request = new ServerRequest( [], @@ -2082,18 +2148,7 @@ public function testRespondToAccessTokenRequestMissingCodeVerifier(): void 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - 'code_challenge' => 'foobar', - 'code_challenge_method' => 'plain', - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -2137,8 +2192,6 @@ public function testAuthCodeRepositoryUniqueConstraintCheck(): void $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); } @@ -2158,13 +2211,13 @@ public function testAuthCodeRepositoryFailToPersist(): void $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $authCodeRepository->method('persistNewAuthCode')->willThrowException(OAuthServerException::serverError('something bad happened')); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); $grant = new AuthCodeGrant( $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); - $grant->setEncryptionKey($this->cryptStub->getKey()); $this->expectException(OAuthServerException::class); $this->expectExceptionCode(7); @@ -2207,6 +2260,7 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck(): void $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -2214,12 +2268,29 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepository->method('getAuthCodeEntity')->willReturn($ace); + $refreshTokenRepositoryMock ->expects(self::exactly(2)) ->method('persistNewRefreshToken') @@ -2232,7 +2303,7 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck(): void }); $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -2240,8 +2311,6 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck(): void $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -2256,16 +2325,7 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck(): void 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -2282,6 +2342,7 @@ public function testRefreshTokenRepositoryFailToPersist(): void $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -2289,15 +2350,32 @@ public function testRefreshTokenRepositoryFailToPersist(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willThrowException(OAuthServerException::serverError('something bad happened')); + $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepositoryMock->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepositoryMock->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -2305,8 +2383,6 @@ public function testRefreshTokenRepositoryFailToPersist(): void $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -2321,16 +2397,7 @@ public function testRefreshTokenRepositoryFailToPersist(): void 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); @@ -2350,6 +2417,7 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): v $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('validateClient')->willReturn(true); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); $scopeEntity = new ScopeEntity(); @@ -2357,15 +2425,32 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): v $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); + $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepositoryMock->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $ace = new AuthCodeEntity(); + $ace->setIdentifier(uniqid()); + $ace->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $ace->setClient($client); + $ace->setRedirectUri(self::REDIRECT_URI); + $user = new UserEntity(); + $user->setIdentifier('123'); + $ace->setUser($user); + $ace->setScopes(['foo']); + + $authCodeRepositoryMock->method('getAuthCodeEntity')->willReturn($ace); + $grant = new AuthCodeGrant( - $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $authCodeRepositoryMock, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), new DateInterval('PT10M') ); @@ -2373,8 +2458,6 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): v $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $request = new ServerRequest( [], @@ -2389,16 +2472,7 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): v 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'redirect_uri' => self::REDIRECT_URI, - 'code' => $this->cryptStub->doEncrypt( - json_encode([ - 'auth_code_id' => uniqid(), - 'expire_time' => time() + 3600, - 'client_id' => 'foo', - 'user_id' => '123', - 'scopes' => ['foo'], - 'redirect_uri' => self::REDIRECT_URI, - ], JSON_THROW_ON_ERROR) - ), + 'code' => $ace->getIdentifier() ] ); diff --git a/tests/Grant/ClientCredentialsGrantTest.php b/tests/Grant/ClientCredentialsGrantTest.php index 69f756c37..1aefc6412 100644 --- a/tests/Grant/ClientCredentialsGrantTest.php +++ b/tests/Grant/ClientCredentialsGrantTest.php @@ -11,6 +11,8 @@ use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use League\OAuth2\Server\RequestAccessTokenEvent; +use League\OAuth2\Server\RequestEvent; use LeagueTests\Stubs\AccessTokenEntity; use LeagueTests\Stubs\ClientEntity; use LeagueTests\Stubs\ScopeEntity; @@ -51,7 +53,17 @@ public function testRespondToRequest(): void $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $accessTokenEventEmitted = false; + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::ACCESS_TOKEN_ISSUED, + function ($event) use (&$accessTokenEventEmitted): void { + self::assertInstanceOf(RequestAccessTokenEvent::class, $event); + + $accessTokenEventEmitted = true; + } + ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -64,5 +76,9 @@ public function testRespondToRequest(): void $response = $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertNotEmpty($response->getAccessToken()->getIdentifier()); + + if (!$accessTokenEventEmitted) { + self::fail('Access token issued event is not emitted.'); + } } } diff --git a/tests/Grant/DeviceCodeGrantTest.php b/tests/Grant/DeviceCodeGrantTest.php index 42157a494..393cfba99 100644 --- a/tests/Grant/DeviceCodeGrantTest.php +++ b/tests/Grant/DeviceCodeGrantTest.php @@ -18,12 +18,16 @@ use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use League\OAuth2\Server\RequestAccessTokenEvent; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\RequestRefreshTokenEvent; use LeagueTests\Stubs\AccessTokenEntity; use LeagueTests\Stubs\ClientEntity; use LeagueTests\Stubs\DeviceCodeEntity; use LeagueTests\Stubs\RefreshTokenEntity; use LeagueTests\Stubs\ScopeEntity; use LeagueTests\Stubs\StubResponseType; +use LeagueTests\Stubs\UserEntity; use PHPUnit\Framework\TestCase; use function base64_encode; @@ -249,6 +253,7 @@ public function testValidateDeviceAuthorizationRequestClientMismatch(): void public function testCompleteDeviceAuthorizationRequest(): void { $deviceCode = new DeviceCodeEntity(); + $deviceCode->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $deviceCode->setIdentifier('deviceCodeEntityIdentifier'); $deviceCode->setUserCode('foo'); @@ -262,9 +267,12 @@ public function testCompleteDeviceAuthorizationRequest(): void 'http://foo/bar', ); - $grant->completeDeviceAuthorizationRequest($deviceCode->getIdentifier(), 'userId', true); + $user = new UserEntity(); + $user->setIdentifier('userId'); - $this::assertEquals('userId', $deviceCode->getUserIdentifier()); + $grant->completeDeviceAuthorizationRequest($deviceCode->getIdentifier(), $user, true); + + $this::assertEquals('userId', $deviceCode->getUser()->getIdentifier()); } public function testDeviceAuthorizationResponse(): void @@ -293,8 +301,6 @@ public function testDeviceAuthorizationResponse(): void $clientRepository, $accessRepositoryMock, $scopeRepositoryMock, - 'file://' . __DIR__ . '/../Stubs/private.key', - base64_encode(random_bytes(36)), new StubResponseType() ); @@ -337,7 +343,10 @@ public function testRespondToAccessTokenRequest(): void $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); $deviceCodeEntity = new DeviceCodeEntity(); - $deviceCodeEntity->setUserIdentifier('baz'); + $user = new UserEntity(); + $user->setIdentifier('baz'); + + $deviceCodeEntity->setUser($user); $deviceCodeEntity->setIdentifier('deviceCodeEntityIdentifier'); $deviceCodeEntity->setUserCode('123456'); $deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('+1 hour')); @@ -349,11 +358,12 @@ public function testRespondToAccessTokenRequest(): void ->willReturn($deviceCodeEntity); $accessTokenEntity = new AccessTokenEntity(); + $accessTokenEntity->setClient($client); $accessTokenEntity->addScope($scope); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken') - ->with($client, $deviceCodeEntity->getScopes(), $deviceCodeEntity->getUserIdentifier()) + ->with($client, $deviceCodeEntity->getScopes(), $deviceCodeEntity->getUser()) ->willReturn($accessTokenEntity); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); @@ -376,9 +386,29 @@ public function testRespondToAccessTokenRequest(): void $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $grant->completeDeviceAuthorizationRequest($deviceCodeEntity->getIdentifier(), 'baz', true); + $grant->completeDeviceAuthorizationRequest($deviceCodeEntity->getIdentifier(), $user, true); + + $accessTokenEventEmitted = false; + $refreshTokenEventEmitted = false; + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::ACCESS_TOKEN_ISSUED, + function ($event) use (&$accessTokenEventEmitted): void { + self::assertInstanceOf(RequestAccessTokenEvent::class, $event); + + $accessTokenEventEmitted = true; + } + ); + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::REFRESH_TOKEN_ISSUED, + function ($event) use (&$refreshTokenEventEmitted): void { + self::assertInstanceOf(RequestRefreshTokenEvent::class, $event); + + $refreshTokenEventEmitted = true; + } + ); $serverRequest = (new ServerRequest())->withParsedBody([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', @@ -391,6 +421,14 @@ public function testRespondToAccessTokenRequest(): void $this::assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); $this::assertSame([$scope], $responseType->getAccessToken()->getScopes()); + + if (!$accessTokenEventEmitted) { + self::fail('Access token issued event is not emitted.'); + } + + if (!$refreshTokenEventEmitted) { + self::fail('Refresh token issued event is not emitted.'); + } } public function testRespondToRequestMissingClient(): void @@ -436,8 +474,10 @@ public function testRespondToRequestMissingDeviceCode(): void $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); + $user = new UserEntity(); + $user->setIdentifier('baz'); $deviceCodeEntity = new DeviceCodeEntity(); - $deviceCodeEntity->setUserIdentifier('baz'); + $deviceCodeEntity->setUser($user); $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); @@ -454,7 +494,6 @@ public function testRespondToRequestMissingDeviceCode(): void $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -501,7 +540,6 @@ public function testIssueSlowDownError(): void $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -548,7 +586,6 @@ public function testIssueAuthorizationPendingError(): void $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -595,7 +632,6 @@ public function testIssueExpiredTokenError(): void $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -737,9 +773,8 @@ public function testIssueAccessDeniedError(): void $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $grant->completeDeviceAuthorizationRequest($deviceCode->getIdentifier(), '1', false); + $grant->completeDeviceAuthorizationRequest($deviceCode->getIdentifier(), new UserEntity(), false); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', diff --git a/tests/Grant/ImplicitGrantTest.php b/tests/Grant/ImplicitGrantTest.php index 617aaa842..939939d99 100644 --- a/tests/Grant/ImplicitGrantTest.php +++ b/tests/Grant/ImplicitGrantTest.php @@ -14,11 +14,12 @@ use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use League\OAuth2\Server\RequestAccessTokenEvent; +use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\RedirectResponse; use LeagueTests\Stubs\AccessTokenEntity; use LeagueTests\Stubs\ClientEntity; -use LeagueTests\Stubs\CryptTraitStub; use LeagueTests\Stubs\ScopeEntity; use LeagueTests\Stubs\StubResponseType; use LeagueTests\Stubs\UserEntity; @@ -31,11 +32,9 @@ class ImplicitGrantTest extends TestCase private const DEFAULT_SCOPE = 'basic'; private const REDIRECT_URI = 'https://foo/bar'; - protected CryptTraitStub $cryptStub; - public function setUp(): void { - $this->cryptStub = new CryptTraitStub(); + } public function testGetIdentifier(): void @@ -258,7 +257,9 @@ public function testCompleteAuthorizationRequest(): void $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); - $accessToken->setUserIdentifier('userId'); + $user = new UserEntity(); + $user->setIdentifier('userId'); + $accessToken->setUser($user); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); @@ -268,11 +269,25 @@ public function testCompleteAuthorizationRequest(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); + $accessTokenEventEmitted = false; + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::ACCESS_TOKEN_ISSUED, + function ($event) use (&$accessTokenEventEmitted): void { + self::assertInstanceOf(RequestAccessTokenEvent::class, $event); + + $accessTokenEventEmitted = true; + } + ); + self::assertInstanceOf(RedirectResponse::class, $grant->completeAuthorizationRequest($authRequest)); + + if (!$accessTokenEventEmitted) { + // self::fail('Access token issued event is not emitted.'); // TODO: next major release + } } public function testCompleteAuthorizationRequestDenied(): void @@ -296,7 +311,6 @@ public function testCompleteAuthorizationRequestDenied(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); @@ -327,7 +341,9 @@ public function testAccessTokenRepositoryUniqueConstraintCheck(): void $accessToken = new AccessTokenEntity(); $accessToken->setClient($client); - $accessToken->setUserIdentifier('userId'); + $user = new UserEntity(); + $user->setIdentifier('userId'); + $accessToken->setUser($user); /** @var AccessTokenRepositoryInterface|MockObject $accessTokenRepositoryMock */ $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); @@ -348,7 +364,6 @@ public function testAccessTokenRepositoryUniqueConstraintCheck(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); @@ -377,7 +392,6 @@ public function testAccessTokenRepositoryFailToPersist(): void $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); @@ -409,7 +423,6 @@ public function testAccessTokenRepositoryFailToPersistUniqueNoInfiniteLoop(): vo $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); $grant = new ImplicitGrant(new DateInterval('PT10M')); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); diff --git a/tests/Grant/PasswordGrantTest.php b/tests/Grant/PasswordGrantTest.php index 8c60a8c78..6f1a7a3bf 100644 --- a/tests/Grant/PasswordGrantTest.php +++ b/tests/Grant/PasswordGrantTest.php @@ -15,6 +15,9 @@ use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; +use League\OAuth2\Server\RequestAccessTokenEvent; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\RequestRefreshTokenEvent; use LeagueTests\Stubs\AccessTokenEntity; use LeagueTests\Stubs\ClientEntity; use LeagueTests\Stubs\RefreshTokenEntity; @@ -46,7 +49,9 @@ public function testRespondToRequest(): void $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); @@ -67,7 +72,27 @@ public function testRespondToRequest(): void $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $accessTokenEventEmitted = false; + $refreshTokenEventEmitted = false; + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::ACCESS_TOKEN_ISSUED, + function ($event) use (&$accessTokenEventEmitted): void { + self::assertInstanceOf(RequestAccessTokenEvent::class, $event); + + $accessTokenEventEmitted = true; + } + ); + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::REFRESH_TOKEN_ISSUED, + function ($event) use (&$refreshTokenEventEmitted): void { + self::assertInstanceOf(RequestRefreshTokenEvent::class, $event); + + $refreshTokenEventEmitted = true; + } + ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -80,6 +105,14 @@ public function testRespondToRequest(): void $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); + + if (!$accessTokenEventEmitted) { + self::fail('Access token issued event is not emitted.'); + } + + if (!$refreshTokenEventEmitted) { + self::fail('Refresh token issued event is not emitted.'); + } } public function testRespondToRequestNullRefreshToken(): void @@ -91,8 +124,11 @@ public function testRespondToRequestNullRefreshToken(): void $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); @@ -112,7 +148,6 @@ public function testRespondToRequestNullRefreshToken(): void $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -167,9 +202,14 @@ public function testRespondToRequestMissingPassword(): void $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity()); + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setDefaultScope(self::DEFAULT_SCOPE); + $grant->setScopeRepository($scopeRepositoryMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', diff --git a/tests/Grant/RefreshTokenGrantTest.php b/tests/Grant/RefreshTokenGrantTest.php index b2dbbadd2..984f898fa 100644 --- a/tests/Grant/RefreshTokenGrantTest.php +++ b/tests/Grant/RefreshTokenGrantTest.php @@ -5,6 +5,7 @@ namespace LeagueTests\Grant; use DateInterval; +use DateTimeImmutable; use Laminas\Diactoros\Response; use Laminas\Diactoros\ServerRequest; use League\OAuth2\Server\CryptKey; @@ -15,13 +16,16 @@ use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use League\OAuth2\Server\RequestAccessTokenEvent; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; use LeagueTests\Stubs\AccessTokenEntity; use LeagueTests\Stubs\ClientEntity; -use LeagueTests\Stubs\CryptTraitStub; use LeagueTests\Stubs\RefreshTokenEntity; use LeagueTests\Stubs\ScopeEntity; use LeagueTests\Stubs\StubResponseType; +use LeagueTests\Stubs\UserEntity; use PHPUnit\Framework\TestCase; use function json_encode; @@ -29,13 +33,6 @@ class RefreshTokenGrantTest extends TestCase { - protected CryptTraitStub $cryptStub; - - public function setUp(): void - { - $this->cryptStub = new CryptTraitStub(); - } - public function testGetIdentifier(): void { $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); @@ -60,45 +57,67 @@ public function testRespondToRequest(): void $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef1'); + $ace->setClient($client); + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($ace); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef1'); + $ace->setClient($client); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $rte = new RefreshTokenEntity(); + $rte->setClient($client); + $rte->setIdentifier('zyxwvu'); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef2'); + $ace->setClient($client); + $rte->setAccessToken($ace); + $rte->setScopes([$scopeEntity]); + $user = new UserEntity(); + $user->setIdentifier('123'); + $rte->setUser($user); + $rte->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $refreshTokenRepositoryMock->method('getRefreshTokenEntity')->willReturn($rte); $refreshTokenRepositoryMock->expects(self::once())->method('persistNewRefreshToken')->willReturnSelf(); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->revokeRefreshTokens(true); - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => '123', - 'expire_time' => time() + 3600, - ] + $accessTokenEventEmitted = false; + $refreshTokenEventEmitted = false; + + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::ACCESS_TOKEN_ISSUED, + function ($event) use (&$accessTokenEventEmitted): void { + self::assertInstanceOf(RequestAccessTokenEvent::class, $event); + + $accessTokenEventEmitted = true; + } ); - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } + $grant->getListenerRegistry()->subscribeTo( + RequestEvent::REFRESH_TOKEN_ISSUED, + function ($event) use (&$refreshTokenEventEmitted): void { + self::assertInstanceOf(RequestRefreshTokenEvent::class, $event); - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken + $refreshTokenEventEmitted = true; + } ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', 'scopes' => ['foo'], ]); @@ -106,6 +125,14 @@ public function testRespondToRequest(): void $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); self::assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); + + if (!$accessTokenEventEmitted) { + self::fail('Access token issued event is not emitted.'); + } + + if (!$refreshTokenEventEmitted) { + self::fail('Refresh token issued event is not emitted.'); + } } public function testRespondToRequestNullRefreshToken(): void @@ -125,11 +152,27 @@ public function testRespondToRequestNullRefreshToken(): void $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $rte = new RefreshTokenEntity(); + $rte->setClient($client); + $rte->setIdentifier('zyxwvu'); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $rte->setAccessToken($ace); + $rte->setScopes([$scopeEntity]); + $user = new UserEntity(); + $user->setIdentifier('123'); + $rte->setUser($user); + $rte->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + + $refreshTokenRepositoryMock->method('getRefreshTokenEntity')->willReturn($rte); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null); $refreshTokenRepositoryMock->expects(self::never())->method('persistNewRefreshToken'); @@ -137,32 +180,11 @@ public function testRespondToRequestNullRefreshToken(): void $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => '123', - 'expire_time' => time() + 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', 'scopes' => ['foo'], ]); @@ -183,13 +205,35 @@ public function testRespondToReducedScopes(): void $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $scopeEntity = new ScopeEntity(); + $scopeEntity->setIdentifier('foo'); + + $rte = new RefreshTokenEntity(); + $rte->setClient($client); + $rte->setIdentifier('zyxwvu'); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $rte->setAccessToken($ace); + $rte->setScopes([$scopeEntity]); + $user = new UserEntity(); + $user->setIdentifier('123'); + $rte->setUser($user); + $rte->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + + $refreshTokenRepositoryMock->method('getRefreshTokenEntity')->willReturn($rte); + $scope = new ScopeEntity(); $scope->setIdentifier('foo'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); @@ -200,33 +244,12 @@ public function testRespondToReducedScopes(): void $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->revokeRefreshTokens(true); - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo', 'bar'], - 'user_id' => '123', - 'expire_time' => time() + 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); - $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', 'scope' => 'foo', ]); @@ -248,9 +271,33 @@ public function testRespondToUnexpectedScope(): void $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $scopeEntity1 = new ScopeEntity(); + $scopeEntity1->setIdentifier('foo'); + + $scopeEntity2 = new ScopeEntity(); + $scopeEntity2->setIdentifier('bar'); + + $rte = new RefreshTokenEntity(); + $rte->setClient($client); + $rte->setIdentifier('zyxwvu'); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $rte->setAccessToken($ace); + $rte->setScopes([$scopeEntity1, $scopeEntity2]); + $user = new UserEntity(); + $user->setIdentifier('123'); + $rte->setUser($user); + $rte->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + + $refreshTokenRepositoryMock->method('getRefreshTokenEntity')->willReturn($rte); $scope = new ScopeEntity(); $scope->setIdentifier('foobar'); @@ -261,32 +308,11 @@ public function testRespondToUnexpectedScope(): void $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo', 'bar'], - 'user_id' => 123, - 'expire_time' => time() + 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', 'scope' => 'foobar', ]); @@ -314,8 +340,6 @@ public function testRespondToRequestMissingOldToken(): void $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -346,8 +370,6 @@ public function testRespondToRequestInvalidOldToken(): void $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $oldRefreshToken = 'foobar'; @@ -365,7 +387,7 @@ public function testRespondToRequestInvalidOldToken(): void $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } - public function testRespondToRequestClientMismatch(): void + public function testRespondToRequestRefreshTokenNotSet(): void { $client = new ClientEntity(); $client->setIdentifier('foo'); @@ -375,41 +397,68 @@ public function testRespondToRequestClientMismatch(): void $clientRepositoryMock->method('getClientEntity')->willReturn($client); $clientRepositoryMock->method('validateClient')->willReturn(true); + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $oldRefreshToken = 'foobar'; + + $serverRequest = (new ServerRequest())->withParsedBody([ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + ]); + + $responseType = new StubResponseType(); + + $this->expectException(OAuthServerException::class); + $this->expectExceptionCode(8); + + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); + } + + public function testRespondToRequestClientMismatch(): void + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + + $client2 = new ClientEntity(); + $client2->setIdentifier('bar'); + $client2->setRedirectUri('http://foo/bar'); + + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client, $client2); + $clientRepositoryMock->method('validateClient')->willReturn(true); + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $scope1 = new ScopeEntity(); + $scope1->setIdentifier('foo'); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope1); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - - $oldRefreshToken = json_encode( - [ - 'client_id' => 'bar', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => 123, - 'expire_time' => time() + 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); + $grant->setScopeRepository($scopeRepositoryMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', ]); $responseType = new StubResponseType(); @@ -427,41 +476,32 @@ public function testRespondToRequestExpiredToken(): void $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('getClientEntity')->willReturn($client, $client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $scope1 = new ScopeEntity(); + $scope1->setIdentifier('foo'); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope1); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => 123, - 'expire_time' => time() - 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); + $grant->setScopeRepository($scopeRepositoryMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', ]); $responseType = new StubResponseType(); @@ -479,42 +519,33 @@ public function testRespondToRequestRevokedToken(): void $client->setRedirectUri('http://foo/bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); - $clientRepositoryMock->method('getClientEntity')->willReturn($client); + $clientRepositoryMock->method('getClientEntity')->willReturn($client, $client); $clientRepositoryMock->method('validateClient')->willReturn(true); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('isRefreshTokenRevoked')->willReturn(true); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $scope1 = new ScopeEntity(); + $scope1->setIdentifier('foo'); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope1); $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => 123, - 'expire_time' => time() + 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); + $grant->setScopeRepository($scopeRepositoryMock); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', ]); $responseType = new StubResponseType(); @@ -543,21 +574,36 @@ public function testRespondToRequestFinalizeScopes(): void $barScopeEntity->setIdentifier('bar'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); - $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($fooScopeEntity, $barScopeEntity); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($fooScopeEntity, $barScopeEntity, $fooScopeEntity, $barScopeEntity); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $rte = new RefreshTokenEntity(); + $rte->setClient($client); + $rte->setIdentifier('zyxwvu'); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $rte->setAccessToken($ace); + $rte->setScopes([$fooScopeEntity, $barScopeEntity]); + $user = new UserEntity(); + $user->setIdentifier('123'); + $rte->setUser($user); + $rte->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + + $refreshTokenRepositoryMock->method('getRefreshTokenEntity')->willReturn($rte); + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $scopes = [$fooScopeEntity, $barScopeEntity]; @@ -569,34 +615,17 @@ public function testRespondToRequestFinalizeScopes(): void ->with($scopes, $grant->getIdentifier(), $client) ->willReturn($finalizedScopes); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); $accessTokenRepositoryMock ->method('getNewToken') ->with($client, $finalizedScopes) - ->willReturn(new AccessTokenEntity()); - - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo', 'bar'], - 'user_id' => '123', - 'expire_time' => time() + 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); + ->willReturn($accessToken); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', 'scope' => 'foo bar', ]); @@ -621,41 +650,43 @@ public function testRevokedRefreshToken(): void $scopeEntity->setIdentifier('foo'); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); - $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity, $scopeEntity); $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($client); + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('isRefreshTokenRevoked') ->will(self::onConsecutiveCalls(false, true)); $refreshTokenRepositoryMock->expects(self::once())->method('revokeRefreshToken')->with(self::equalTo($refreshTokenId)); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => $refreshTokenId, - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => '123', - 'expire_time' => time() + 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } + $rte = new RefreshTokenEntity(); + $rte->setClient($client); + $rte->setIdentifier($refreshTokenId); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $rte->setAccessToken($ace); + $rte->setScopes([$scopeEntity]); + $user = new UserEntity(); + $user->setIdentifier('123'); + $rte->setUser($user); + $rte->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); + $refreshTokenRepositoryMock->method('getRefreshTokenEntity')->willReturn($rte); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => $refreshTokenId, 'scope' => 'foo', ]); @@ -663,8 +694,6 @@ public function testRevokedRefreshToken(): void $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->revokeRefreshTokens(true); $grant->respondToAccessTokenRequest($serverRequest, new StubResponseType(), new DateInterval('PT5M')); @@ -696,51 +725,43 @@ public function testUnrevokedRefreshToken(): void $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessTokenEntity); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->method('isRefreshTokenRevoked')->willReturn(false); $refreshTokenRepositoryMock->expects(self::never())->method('revokeRefreshToken'); - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => $refreshTokenId, - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => '123', - 'expire_time' => time() + 3600, - ] - ); + $rte = new RefreshTokenEntity(); + $rte->setClient($client); + $rte->setIdentifier('zyxwvu'); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $rte->setAccessToken($ace); + $rte->setScopes([$scopeEntity]); + $user = new UserEntity(); + $user->setIdentifier('123'); + $rte->setUser($user); + $rte->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); + $refreshTokenRepositoryMock->method('getRefreshTokenEntity')->willReturn($rte); $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => $refreshTokenId, 'scope' => 'foo', ]); - $privateKey = new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'); - $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey($privateKey); $grant->revokeRefreshTokens(false); $responseType = new BearerTokenResponse(); - $responseType->setPrivateKey($privateKey); - $responseType->setEncryptionKey($this->cryptStub->getKey()); $response = $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')) ->generateHttpResponse(new Response()); @@ -752,7 +773,7 @@ public function testUnrevokedRefreshToken(): void self::assertObjectHasProperty('expires_in', $json); self::assertObjectHasProperty('access_token', $json); self::assertObjectHasProperty('refresh_token', $json); - self::assertNotSame($json->refresh_token, $encryptedOldRefreshToken); + self::assertNotSame($json->refresh_token, $refreshTokenId); } public function testRespondToRequestWithIntUserId(): void @@ -772,44 +793,42 @@ public function testRespondToRequestWithIntUserId(): void $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); - $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenEntity = new AccessTokenEntity(); + $accessTokenEntity->setClient($client); + $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessTokenEntity); $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf(); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $accessTokenRepositoryMock->method('getAccessTokenEntity')->willReturn($ace); $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $refreshTokenRepositoryMock->expects(self::once())->method('persistNewRefreshToken')->willReturnSelf(); + $rte = new RefreshTokenEntity(); + $rte->setClient($client); + $rte->setIdentifier('zyxwvu'); + $ace = new AccessTokenEntity(); + $ace->setIdentifier('abcdef'); + $rte->setAccessToken($ace); + $rte->setScopes([$scopeEntity]); + $user = new UserEntity(); + $user->setIdentifier('123'); + $rte->setUser($user); + $rte->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); + + $refreshTokenRepositoryMock->method('getRefreshTokenEntity')->willReturn($rte); + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); $grant->setAccessTokenRepository($accessTokenRepositoryMock); - $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $grant->revokeRefreshTokens(true); - $oldRefreshToken = json_encode( - [ - 'client_id' => 'foo', - 'refresh_token_id' => 'zyxwvu', - 'access_token_id' => 'abcdef', - 'scopes' => ['foo'], - 'user_id' => 123, - 'expire_time' => time() + 3600, - ] - ); - - if ($oldRefreshToken === false) { - self::fail('json_encode failed'); - } - - $encryptedOldRefreshToken = $this->cryptStub->doEncrypt( - $oldRefreshToken - ); - $serverRequest = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', 'client_secret' => 'bar', - 'refresh_token' => $encryptedOldRefreshToken, + 'refresh_token' => 'zyxwvu', 'scopes' => ['foo'], ]); diff --git a/tests/Middleware/AuthorizationServerMiddlewareTest.php b/tests/Middleware/AuthorizationServerMiddlewareTest.php index 814e96a6c..1e6383570 100644 --- a/tests/Middleware/AuthorizationServerMiddlewareTest.php +++ b/tests/Middleware/AuthorizationServerMiddlewareTest.php @@ -50,8 +50,6 @@ public function testValidResponse(): void $clientRepository, $accessRepositoryMock, $scopeRepositoryMock, - 'file://' . __DIR__ . '/../Stubs/private.key', - base64_encode(random_bytes(36)), new StubResponseType() ); @@ -84,8 +82,6 @@ public function testOAuthErrorResponse(): void $clientRepository, $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), - 'file://' . __DIR__ . '/../Stubs/private.key', - base64_encode(random_bytes(36)), new StubResponseType() ); diff --git a/tests/Middleware/ResourceServerMiddlewareTest.php b/tests/Middleware/ResourceServerMiddlewareTest.php index 4a6d3b79e..95492592f 100644 --- a/tests/Middleware/ResourceServerMiddlewareTest.php +++ b/tests/Middleware/ResourceServerMiddlewareTest.php @@ -14,6 +14,7 @@ use League\OAuth2\Server\ResourceServer; use LeagueTests\Stubs\AccessTokenEntity; use LeagueTests\Stubs\ClientEntity; +use LeagueTests\Stubs\UserEntity; use PHPUnit\Framework\TestCase; use function func_get_args; @@ -33,10 +34,12 @@ public function testValidResponse(): void $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('test'); - $accessToken->setUserIdentifier('123'); + $user = new UserEntity(); + $user->setIdentifier('123'); + $accessToken->setUser($user); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); - $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $accessToken->setSigner('RS256', new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $token = $accessToken->toString(); @@ -68,10 +71,12 @@ public function testValidResponseExpiredToken(): void $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('test'); - $accessToken->setUserIdentifier('123'); + $user = new UserEntity(); + $user->setIdentifier('123'); + $accessToken->setUser($user); $accessToken->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H'))); $accessToken->setClient($client); - $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $accessToken->setSigner('RS256', new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $token = $accessToken->toString(); diff --git a/tests/ResponseTypes/BearerResponseTypeTest.php b/tests/ResponseTypes/BearerResponseTypeTest.php index 386fb628b..c10cc6993 100644 --- a/tests/ResponseTypes/BearerResponseTypeTest.php +++ b/tests/ResponseTypes/BearerResponseTypeTest.php @@ -17,6 +17,7 @@ use LeagueTests\Stubs\ClientEntity; use LeagueTests\Stubs\RefreshTokenEntity; use LeagueTests\Stubs\ScopeEntity; +use LeagueTests\Stubs\UserEntity; use PHPUnit\Framework\TestCase; use function base64_encode; @@ -29,8 +30,6 @@ class BearerResponseTypeTest extends TestCase public function testGenerateHttpResponse(): void { $responseType = new BearerTokenResponse(); - $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); @@ -43,8 +42,10 @@ public function testGenerateHttpResponse(): void $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->addScope($scope); - $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $accessToken->setUserIdentifier('userId'); + $accessToken->setSigner('RS256', new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $user = new UserEntity(); + $user->setIdentifier('userId'); + $accessToken->setUser($user); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); @@ -72,8 +73,6 @@ public function testGenerateHttpResponse(): void public function testGenerateHttpResponseWithExtraParams(): void { $responseType = new BearerTokenResponseWithParams(); - $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); @@ -86,8 +85,10 @@ public function testGenerateHttpResponseWithExtraParams(): void $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); $accessToken->addScope($scope); - $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $accessToken->setUserIdentifier('userId'); + $accessToken->setSigner('RS256', new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $user = new UserEntity(); + $user->setIdentifier('userId'); + $accessToken->setUser($user); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); @@ -118,18 +119,18 @@ public function testGenerateHttpResponseWithExtraParams(): void public function testDetermineAccessTokenInHeaderValidToken(): void { $responseType = new BearerTokenResponse(); - $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); - $accessToken->setUserIdentifier('123'); + $user = new UserEntity(); + $user->setIdentifier('123'); + $accessToken->setUser($user); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); - $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $accessToken->setSigner('RS256', new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); @@ -163,18 +164,18 @@ public function testDetermineAccessTokenInHeaderInvalidJWT(): void $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $responseType = new BearerTokenResponse(); - $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); - $accessToken->setUserIdentifier('123'); + $user = new UserEntity(); + $user->setIdentifier('123'); + $accessToken->setUser($user); $accessToken->setExpiryDateTime((new DateTimeImmutable())->sub(new DateInterval('PT1H'))); $accessToken->setClient($client); - $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $accessToken->setSigner('RS256', new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); @@ -205,18 +206,18 @@ public function testDetermineAccessTokenInHeaderInvalidJWT(): void public function testDetermineAccessTokenInHeaderRevokedToken(): void { $responseType = new BearerTokenResponse(); - $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); $accessToken = new AccessTokenEntity(); $accessToken->setIdentifier('abcdef'); - $accessToken->setUserIdentifier('123'); + $user = new UserEntity(); + $user->setIdentifier('userId'); + $accessToken->setUser($user); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add(new DateInterval('PT1H'))); $accessToken->setClient($client); - $accessToken->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $accessToken->setSigner('RS256', new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); $refreshToken = new RefreshTokenEntity(); $refreshToken->setIdentifier('abcdef'); @@ -250,8 +251,6 @@ public function testDetermineAccessTokenInHeaderRevokedToken(): void public function testDetermineAccessTokenInHeaderInvalidToken(): void { $responseType = new BearerTokenResponse(); - $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); @@ -273,8 +272,6 @@ public function testDetermineAccessTokenInHeaderInvalidToken(): void public function testDetermineMissingBearerInHeader(): void { $responseType = new BearerTokenResponse(); - $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); diff --git a/tests/ResponseTypes/DeviceCodeResponseTypeTest.php b/tests/ResponseTypes/DeviceCodeResponseTypeTest.php index 93bd9d6b3..05574f5b8 100644 --- a/tests/ResponseTypes/DeviceCodeResponseTypeTest.php +++ b/tests/ResponseTypes/DeviceCodeResponseTypeTest.php @@ -23,8 +23,6 @@ class DeviceCodeResponseTypeTest extends TestCase public function testGenerateHttpResponse(): void { $responseType = new DeviceCodeResponse(); - $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); - $responseType->setEncryptionKey(base64_encode(random_bytes(36))); $client = new ClientEntity(); $client->setIdentifier('clientName'); diff --git a/tests/Stubs/CryptTraitStub.php b/tests/Stubs/CryptTraitStub.php deleted file mode 100644 index 19de09008..000000000 --- a/tests/Stubs/CryptTraitStub.php +++ /dev/null @@ -1,36 +0,0 @@ -setEncryptionKey(base64_encode(random_bytes(36))); - } - - public function getKey(): string|Key|null - { - return $this->encryptionKey; - } - - public function doEncrypt(string $unencryptedData): string - { - return $this->encrypt($unencryptedData); - } - - public function doDecrypt(string $encryptedData): string - { - return $this->decrypt($encryptedData); - } -} diff --git a/tests/Stubs/GrantType.php b/tests/Stubs/GrantType.php index 16eab4795..dcfa48d90 100644 --- a/tests/Stubs/GrantType.php +++ b/tests/Stubs/GrantType.php @@ -7,6 +7,7 @@ use DateInterval; use Defuse\Crypto\Key; use League\OAuth2\Server\CryptKeyInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\EventEmitting\EventEmitter; use League\OAuth2\Server\Grant\GrantTypeInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; @@ -91,10 +92,6 @@ public function setDefaultScope(string $scope): void { } - public function setPrivateKey(CryptKeyInterface $privateKey): void - { - } - public function setEncryptionKey(Key|string|null $key = null): void { } @@ -108,7 +105,7 @@ public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $r return true; } - public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void + public function completeDeviceAuthorizationRequest(string $deviceCode, UserEntityInterface $user, bool $userApproved): void { } diff --git a/tests/Stubs/RefreshTokenEntity.php b/tests/Stubs/RefreshTokenEntity.php index 9d6d79f27..2c378f353 100644 --- a/tests/Stubs/RefreshTokenEntity.php +++ b/tests/Stubs/RefreshTokenEntity.php @@ -7,9 +7,11 @@ use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait; +use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; class RefreshTokenEntity implements RefreshTokenEntityInterface { use RefreshTokenTrait; use EntityTrait; + use TokenEntityTrait; } diff --git a/tests/Utils/CryptTraitTest.php b/tests/Utils/CryptTraitTest.php deleted file mode 100644 index b49b0e9e2..000000000 --- a/tests/Utils/CryptTraitTest.php +++ /dev/null @@ -1,46 +0,0 @@ -cryptStub = new CryptTraitStub(); - } - - public function testEncryptDecryptWithPassword(): void - { - $this->cryptStub->setEncryptionKey(base64_encode(random_bytes(36))); - - $this->encryptDecrypt(); - } - - public function testEncryptDecryptWithKey(): void - { - $this->cryptStub->setEncryptionKey(Key::createNewRandomKey()); - - $this->encryptDecrypt(); - } - - private function encryptDecrypt(): void - { - $payload = 'alex loves whisky'; - $encrypted = $this->cryptStub->doEncrypt($payload); - $plainText = $this->cryptStub->doDecrypt($encrypted); - - self::assertNotEquals($payload, $encrypted); - self::assertEquals($payload, $plainText); - } -}