diff --git a/.github/workflows/backwards-compatibility.yml b/.github/workflows/backwards-compatibility.yml index e15e132b5..cc1971dc9 100644 --- a/.github/workflows/backwards-compatibility.yml +++ b/.github/workflows/backwards-compatibility.yml @@ -14,7 +14,8 @@ jobs: uses: "actions/checkout@v2" with: fetch-depth: 0 - + - name: Fix git safe.directory in container + run: mkdir -p /home/runner/work/_temp/_github_home && printf "[safe]\n\tdirectory = /github/workspace" > /home/runner/work/_temp/_github_home/.gitconfig - name: "Backwards Compatibility Check" uses: docker://nyholm/roave-bc-check-ga with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f1db9e1b..ade0c245b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,19 +8,29 @@ on: jobs: tests: - runs-on: ubuntu-latest - strategy: fail-fast: false matrix: - php: [7.2, 7.3, 7.4, 8.0] + php: [8.1, 8.2] + os: [ubuntu-22.04] stability: [prefer-lowest, prefer-stable] + include: + - os: ubuntu-20.04 + php: 8.0 + stability: prefer-lowest + - os: ubuntu-20.04 + php: 8.0 + stability: prefer-stable + + runs-on: ${{ matrix.os }} name: PHP ${{ matrix.php }} - ${{ matrix.stability }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -30,13 +40,17 @@ jobs: coverage: pcov - name: Install dependencies - run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + run: + composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + + - name: Install Scrutinizer/Ocular + run: + composer global require scrutinizer/ocular - name: Execute tests run: vendor/bin/phpunit --verbose --coverage-clover=coverage.clover - name: Code coverage - if: ${{ github.ref == 'refs/heads/master' && matrix.php != 8.0 && github.repository == 'thephpleague/oauth2-server' }} - run: | - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.clover + if: ${{ github.ref == 'refs/heads/master' && github.repository == 'thephpleague/oauth2-server' }} + run: + ~/.composer/vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover diff --git a/CHANGELOG.md b/CHANGELOG.md index b83689127..32aaabedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ 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] +### Changed +- Bumped the versions for laminas/diactoros and psr/http-message to support +PSR-7 v2.0 (PR #1339) + +## [8.5.1] - released 2023-04-04 +### Fixed +- Fixed PHP version constraints and lcobucci/clock version constraint to support PHP 8.1 (PR #1336) + +## [8.5.0] - released 2023-04-03 +### Added +- Support for PHP 8.1 and 8.2 (PR #1333) + +### Removed +- Support PHP 7.2, 7.3, and 7.4 (PR #1333) + +## [8.4.1] - released 2023-03-22 +### Fixed +- Fix deprecation notices for PHP 8.x (PR #1329) + +## [8.4.0] - released 2023-02-15 +### Added +- You can now set a leeway for time drift between servers when validating a JWT (PR #1304) + +### Security +- Access token requests that contain a code_verifier but are not bound to a code_challenge will be rejected to prevent +a PKCE downgrade attack (PR #1326) + +## [8.3.6] - released 2022-11-14 +### Fixed +- Use LooseValidAt instead of StrictValidAt so that users aren't forced to use claims such as NBF in their JWT tokens (PR #1312) + +## [8.3.5] - released 2022-05-12 ### Fixed - Use InMemory::plainText('empty', 'empty') instead of InMemory::plainText('') to avoid [new empty string exception](https://github.com/lcobucci/jwt/pull/833) thrown by lcobucci/jwt (PR #1282) @@ -562,7 +594,13 @@ Version 5 is a complete code rewrite. - First major release -[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.3.4...HEAD +[Unreleased]: https://github.com/thephpleague/oauth2-server/compare/8.5.1...HEAD +[8.5.1]: https://github.com/thephpleague/oauth2-server/compare/8.5.0...8.5.1 +[8.5.0]: https://github.com/thephpleague/oauth2-server/compare/8.4.1...8.5.0 +[8.4.1]: https://github.com/thephpleague/oauth2-server/compare/8.4.0...8.4.1 +[8.4.0]: https://github.com/thephpleague/oauth2-server/compare/8.3.6...8.4.0 +[8.3.6]: https://github.com/thephpleague/oauth2-server/compare/8.3.5...8.3.6 +[8.3.5]: https://github.com/thephpleague/oauth2-server/compare/8.3.4...8.3.5 [8.3.4]: https://github.com/thephpleague/oauth2-server/compare/8.3.3...8.3.4 [8.3.3]: https://github.com/thephpleague/oauth2-server/compare/8.3.2...8.3.3 [8.3.2]: https://github.com/thephpleague/oauth2-server/compare/8.3.1...8.3.2 diff --git a/README.md b/README.md index 5307b840a..84a8584a5 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,9 @@ This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](ht The latest version of this package supports the following versions of PHP: -* PHP 7.2 -* PHP 7.3 -* PHP 7.4 * PHP 8.0 +* PHP 8.1 +* PHP 8.2 The `openssl` and `json` extensions are also required. diff --git a/composer.json b/composer.json index 201986987..e99a43b77 100644 --- a/composer.json +++ b/composer.json @@ -4,18 +4,18 @@ "homepage": "https://oauth2.thephpleague.com/", "license": "MIT", "require": { - "php": "^7.2 || ^8.0", + "php": "^8.0", "ext-openssl": "*", "league/event": "^3.0", - "league/uri": "^6.4", - "lcobucci/jwt": "^3.4.6 || ^4.0.4", - "psr/http-message": "^1.0.1", - "defuse/php-encryption": "^2.2.1", - "ext-json": "*" + "league/uri": "^6.7", + "lcobucci/jwt": "^4.3 || ^5.0", + "psr/http-message": "^1.0.1 || ^2.0", + "defuse/php-encryption": "^2.3", + "lcobucci/clock": "^2.2 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^8.5.13", - "laminas/laminas-diactoros": "^2.4.1", + "phpunit/phpunit": "^9.6.6", + "laminas/laminas-diactoros": "^3.0.0", "phpstan/phpstan": "^0.12.57", "phpstan/phpstan-phpunit": "^0.12.16", "roave/security-advisories": "dev-master" diff --git a/examples/public/client_credentials.php b/examples/public/client_credentials.php index 51a1ca0b7..1e5f090d7 100644 --- a/examples/public/client_credentials.php +++ b/examples/public/client_credentials.php @@ -53,20 +53,16 @@ ]); $app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { - /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { - // Try to respond to the request return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { - // All instances of OAuthServerException can be formatted into a HTTP response return $exception->generateHttpResponse($response); } catch (\Exception $exception) { - // Unknown exception $body = new Stream('php://temp', 'r+'); $body->write($exception->getMessage()); diff --git a/examples/public/password.php b/examples/public/password.php index 6857e988a..db65d7840 100644 --- a/examples/public/password.php +++ b/examples/public/password.php @@ -17,7 +17,6 @@ $app = new App([ // Add the authorization server to the DI container AuthorizationServer::class => function () { - // Setup the authorization server $server = new AuthorizationServer( new ClientRepository(), // instance of ClientRepositoryInterface @@ -46,20 +45,16 @@ $app->post( '/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { - /* @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get(AuthorizationServer::class); try { - // Try to respond to the access token request return $server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { - // All instances of OAuthServerException can be converted to a PSR-7 response return $exception->generateHttpResponse($response); } catch (\Exception $exception) { - // Catch unexpected exceptions $body = $response->getBody(); $body->write($exception->getMessage()); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5f8851b28..9ab509138 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,21 @@ - - - - ./tests/ - - - - - src - - + + + + src + + + + + ./tests/ + + diff --git a/src/AuthorizationValidators/BearerTokenValidator.php b/src/AuthorizationValidators/BearerTokenValidator.php index 3c16f6850..5da9ba8c6 100644 --- a/src/AuthorizationValidators/BearerTokenValidator.php +++ b/src/AuthorizationValidators/BearerTokenValidator.php @@ -14,9 +14,8 @@ use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; +use Lcobucci\JWT\Validation\Constraint\LooseValidAt; use Lcobucci\JWT\Validation\Constraint\SignedWith; -use Lcobucci\JWT\Validation\Constraint\StrictValidAt; -use Lcobucci\JWT\Validation\Constraint\ValidAt; use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\CryptTrait; @@ -43,12 +42,19 @@ class BearerTokenValidator implements AuthorizationValidatorInterface */ private $jwtConfiguration; + /** + * @var \DateInterval|null + */ + private $jwtValidAtDateLeeway; + /** * @param AccessTokenRepositoryInterface $accessTokenRepository + * @param \DateInterval|null $jwtValidAtDateLeeway */ - public function __construct(AccessTokenRepositoryInterface $accessTokenRepository) + public function __construct(AccessTokenRepositoryInterface $accessTokenRepository, \DateInterval $jwtValidAtDateLeeway = null) { $this->accessTokenRepository = $accessTokenRepository; + $this->jwtValidAtDateLeeway = $jwtValidAtDateLeeway; } /** @@ -73,10 +79,9 @@ private function initJwtConfiguration() InMemory::plainText('empty', 'empty') ); + $clock = new SystemClock(new DateTimeZone(\date_default_timezone_get())); $this->jwtConfiguration->setValidationConstraints( - \class_exists(StrictValidAt::class) - ? new StrictValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))) - : new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))), + new LooseValidAt($clock, $this->jwtValidAtDateLeeway), new SignedWith( new Sha256(), InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '') @@ -108,7 +113,7 @@ public function validateAuthorization(ServerRequestInterface $request) $constraints = $this->jwtConfiguration->validationConstraints(); $this->jwtConfiguration->validator()->assert($token, ...$constraints); } catch (RequiredConstraintsViolated $exception) { - throw OAuthServerException::accessDenied('Access token could not be verified'); + throw OAuthServerException::accessDenied('Access token could not be verified', null, $exception); } $claims = $token->claims(); diff --git a/src/Entities/Traits/ClientTrait.php b/src/Entities/Traits/ClientTrait.php index a0078d8d7..370163c35 100644 --- a/src/Entities/Traits/ClientTrait.php +++ b/src/Entities/Traits/ClientTrait.php @@ -30,6 +30,7 @@ trait ClientTrait * Get the client's name. * * @return string + * * @codeCoverageIgnore */ public function getName() diff --git a/src/Entities/Traits/ScopeTrait.php b/src/Entities/Traits/ScopeTrait.php index a132234fc..7eacc3359 100644 --- a/src/Entities/Traits/ScopeTrait.php +++ b/src/Entities/Traits/ScopeTrait.php @@ -16,6 +16,7 @@ trait ScopeTrait * * @return string */ + #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->getIdentifier(); diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index 74325f3eb..0178955a0 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -294,8 +294,8 @@ protected function validateRedirectUri( /** * Validate scopes in the request. * - * @param string|array $scopes - * @param string $redirectUri + * @param string|array|null $scopes + * @param string $redirectUri * * @throws OAuthServerException * diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index 3fac0344e..8336cf649 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -127,39 +127,18 @@ public function respondToAccessTokenRequest( throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e); } - // Validate code challenge - if (!empty($authCodePayload->code_challenge)) { - $codeVerifier = $this->getRequestParameter('code_verifier', $request, null); - - if ($codeVerifier === null) { - throw OAuthServerException::invalidRequest('code_verifier'); - } + $codeVerifier = $this->getRequestParameter('code_verifier', $request, null); - // Validate code_verifier according to RFC-7636 - // @see: https://tools.ietf.org/html/rfc7636#section-4.1 - if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) { - throw OAuthServerException::invalidRequest( - 'code_verifier', - 'Code Verifier must follow the specifications of RFC-7636.' - ); - } + // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack + if (empty($authCodePayload->code_challenge) && $codeVerifier !== null) { + throw OAuthServerException::invalidRequest( + 'code_challenge', + 'code_verifier received when no code_challenge is present' + ); + } - if (\property_exists($authCodePayload, 'code_challenge_method')) { - if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) { - $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method]; - - if ($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 (!empty($authCodePayload->code_challenge)) { + $this->validateCodeChallenge($authCodePayload, $codeVerifier); } // Issue and persist new access token @@ -181,6 +160,39 @@ public function respondToAccessTokenRequest( return $responseType; } + private function validateCodeChallenge($authCodePayload, $codeVerifier) + { + if ($codeVerifier === null) { + throw OAuthServerException::invalidRequest('code_verifier'); + } + + // Validate code_verifier according to RFC-7636 + // @see: https://tools.ietf.org/html/rfc7636#section-4.1 + if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) { + throw OAuthServerException::invalidRequest( + 'code_verifier', + 'Code Verifier must follow the specifications of RFC-7636.' + ); + } + + if (\property_exists($authCodePayload, 'code_challenge_method')) { + if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) { + $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method]; + + if ($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 + ) + ); + } + } + } + /** * Validate the authorization code. * diff --git a/src/RequestAccessTokenEvent.php b/src/RequestAccessTokenEvent.php index 99d17bf36..c2f478284 100644 --- a/src/RequestAccessTokenEvent.php +++ b/src/RequestAccessTokenEvent.php @@ -31,6 +31,7 @@ public function __construct($name, ServerRequestInterface $request, AccessTokenE /** * @return AccessTokenEntityInterface + * * @codeCoverageIgnore */ public function getAccessToken() diff --git a/src/RequestEvent.php b/src/RequestEvent.php index f9dd2a237..7c08e0b90 100644 --- a/src/RequestEvent.php +++ b/src/RequestEvent.php @@ -40,6 +40,7 @@ public function __construct($name, ServerRequestInterface $request) /** * @return ServerRequestInterface + * * @codeCoverageIgnore */ public function getRequest() diff --git a/src/RequestRefreshTokenEvent.php b/src/RequestRefreshTokenEvent.php index 0682e57f5..326a115ed 100644 --- a/src/RequestRefreshTokenEvent.php +++ b/src/RequestRefreshTokenEvent.php @@ -31,6 +31,7 @@ public function __construct($name, ServerRequestInterface $request, RefreshToken /** * @return RefreshTokenEntityInterface + * * @codeCoverageIgnore */ public function getRefreshToken() diff --git a/tests/AuthorizationServerTest.php b/tests/AuthorizationServerTest.php index 1ba4ad9cc..af8c89d8a 100644 --- a/tests/AuthorizationServerTest.php +++ b/tests/AuthorizationServerTest.php @@ -32,6 +32,7 @@ class AuthorizationServerTest extends TestCase { const DEFAULT_SCOPE = 'basic'; + const REDIRECT_URI = 'https://foo/bar'; public function setUp(): void { @@ -86,7 +87,7 @@ public function testRespondToRequest() $client = new ClientEntity(); $client->setConfidential(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepository->method('getClientEntity')->willReturn($client); @@ -245,9 +246,12 @@ public function testCompleteAuthorizationRequest() $server->enableGrantType($grant); + $client = new ClientEntity(); + $client->setRedirectUri(self::REDIRECT_URI); + $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); - $authRequest->setClient(new ClientEntity()); + $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); @@ -260,7 +264,7 @@ public function testCompleteAuthorizationRequest() public function testValidateAuthorizationRequest() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); diff --git a/tests/AuthorizationValidators/BearerTokenValidatorTest.php b/tests/AuthorizationValidators/BearerTokenValidatorTest.php index 838d2bbae..94899ddd4 100644 --- a/tests/AuthorizationValidators/BearerTokenValidatorTest.php +++ b/tests/AuthorizationValidators/BearerTokenValidatorTest.php @@ -71,4 +71,67 @@ public function testBearerTokenValidatorRejectsExpiredToken() $bearerTokenValidator->validateAuthorization($request); } + + public function testBearerTokenValidatorAcceptsExpiredTokenWithinLeeway() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + // We fake generating this token 10 seconds into the future, an extreme example of possible time drift between servers + $future = (new DateTimeImmutable())->add(new DateInterval('PT10S')); + + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock, new \DateInterval('PT10S')); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); + + $jwtTokenFromFutureWithinLeeway = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt($future) + ->canOnlyBeUsedAfter($future) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); + + $request = (new ServerRequest())->withHeader('authorization', \sprintf('Bearer %s', $jwtTokenFromFutureWithinLeeway->toString())); + + $validRequest = $bearerTokenValidator->validateAuthorization($request); + + $this->assertArrayHasKey('authorization', $validRequest->getHeaders()); + } + + public function testBearerTokenValidatorRejectsExpiredTokenBeyondLeeway() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + // We fake generating this token 20 seconds into the future, an extreme example of possible time drift between servers + $future = (new DateTimeImmutable())->add(new DateInterval('PT20S')); + + $bearerTokenValidator = new BearerTokenValidator($accessTokenRepositoryMock, new \DateInterval('PT10S')); + $bearerTokenValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $bearerTokenValidatorReflection = new ReflectionClass(BearerTokenValidator::class); + $jwtConfiguration = $bearerTokenValidatorReflection->getProperty('jwtConfiguration'); + $jwtConfiguration->setAccessible(true); + + $jwtTokenFromFutureBeyondLeeway = $jwtConfiguration->getValue($bearerTokenValidator)->builder() + ->permittedFor('client-id') + ->identifiedBy('token-id') + ->issuedAt($future) + ->canOnlyBeUsedAfter($future) + ->expiresAt((new DateTimeImmutable())->add(new DateInterval('PT1H'))) + ->relatedTo('user-id') + ->withClaim('scopes', 'scope1 scope2 scope3 scope4') + ->getToken(new Sha256(), InMemory::file(__DIR__ . '/../Stubs/private.key')); + + $request = (new ServerRequest())->withHeader('authorization', \sprintf('Bearer %s', $jwtTokenFromFutureBeyondLeeway->toString())); + + $this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class); + $this->expectExceptionCode(9); + + $bearerTokenValidator->validateAuthorization($request); + } } diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php index b02cb7be4..a31ef6f34 100644 --- a/tests/Bootstrap.php +++ b/tests/Bootstrap.php @@ -1,5 +1,7 @@ setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -115,7 +116,7 @@ public function testValidateAuthorizationRequest() [ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ); @@ -125,7 +126,7 @@ public function testValidateAuthorizationRequest() public function testValidateAuthorizationRequestRedirectUriArray() { $client = new ClientEntity(); - $client->setRedirectUri(['http://foo/bar']); + $client->setRedirectUri([self::REDIRECT_URI]); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -154,7 +155,7 @@ public function testValidateAuthorizationRequestRedirectUriArray() [ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ); @@ -164,7 +165,7 @@ public function testValidateAuthorizationRequestRedirectUriArray() public function testValidateAuthorizationRequestWithoutRedirectUri() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); @@ -206,7 +207,7 @@ public function testValidateAuthorizationRequestWithoutRedirectUri() public function testValidateAuthorizationRequestCodeChallenge() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -235,7 +236,7 @@ public function testValidateAuthorizationRequestCodeChallenge() [ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => self::CODE_CHALLENGE, ] ); @@ -246,7 +247,7 @@ public function testValidateAuthorizationRequestCodeChallenge() public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooShort() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -261,7 +262,7 @@ public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooSho $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => \str_repeat('A', 42), ]); @@ -273,7 +274,7 @@ public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooSho public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooLong() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -288,7 +289,7 @@ public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooLon $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => \str_repeat('A', 129), ]); @@ -300,7 +301,7 @@ public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooLon public function testValidateAuthorizationRequestCodeChallengeInvalidCharacters() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -315,7 +316,7 @@ public function testValidateAuthorizationRequestCodeChallengeInvalidCharacters() $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => \str_repeat('A', 42) . '!', ]); @@ -371,7 +372,7 @@ public function testValidateAuthorizationRequestInvalidClientId() public function testValidateAuthorizationRequestBadRedirectUriString() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -397,7 +398,7 @@ public function testValidateAuthorizationRequestBadRedirectUriString() public function testValidateAuthorizationRequestBadRedirectUriArray() { $client = new ClientEntity(); - $client->setRedirectUri(['http://foo/bar']); + $client->setRedirectUri([self::REDIRECT_URI]); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -423,7 +424,7 @@ public function testValidateAuthorizationRequestBadRedirectUriArray() public function testValidateAuthorizationRequestInvalidCodeChallengeMethod() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -444,7 +445,7 @@ public function testValidateAuthorizationRequestInvalidCodeChallengeMethod() $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'foobar', 'code_challenge_method' => 'foo', ]); @@ -457,9 +458,12 @@ public function testValidateAuthorizationRequestInvalidCodeChallengeMethod() public function testCompleteAuthorizationRequest() { + $client = new ClientEntity(); + $client->setRedirectUri(self::REDIRECT_URI); + $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); - $authRequest->setClient(new ClientEntity()); + $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); @@ -478,9 +482,12 @@ public function testCompleteAuthorizationRequest() public function testCompleteAuthorizationRequestDenied() { + $client = new ClientEntity(); + $client->setRedirectUri(self::REDIRECT_URI); + $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(false); - $authRequest->setClient(new ClientEntity()); + $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); @@ -504,7 +511,7 @@ public function testRespondToAccessTokenRequest() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -546,7 +553,7 @@ public function testRespondToAccessTokenRequest() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -555,7 +562,7 @@ public function testRespondToAccessTokenRequest() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -572,7 +579,7 @@ public function testRespondToAccessTokenRequest() public function testRespondToAccessTokenRequestUsingHttpBasicAuth() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setIdentifier('foo'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -612,7 +619,7 @@ public function testRespondToAccessTokenRequestUsingHttpBasicAuth() [], [ 'grant_type' => 'authorization_code', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -621,7 +628,7 @@ public function testRespondToAccessTokenRequestUsingHttpBasicAuth() 'expire_time' => \time() + 3600, 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -639,7 +646,7 @@ public function testRespondToAccessTokenRequestForPublicClient() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -680,7 +687,7 @@ public function testRespondToAccessTokenRequestForPublicClient() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -689,7 +696,7 @@ public function testRespondToAccessTokenRequestForPublicClient() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -707,7 +714,7 @@ public function testRespondToAccessTokenRequestNullRefreshToken() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -748,7 +755,7 @@ public function testRespondToAccessTokenRequestNullRefreshToken() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -757,7 +764,7 @@ public function testRespondToAccessTokenRequestNullRefreshToken() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -775,7 +782,7 @@ public function testRespondToAccessTokenRequestCodeChallengePlain() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -818,7 +825,7 @@ public function testRespondToAccessTokenRequestCodeChallengePlain() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, 'code' => $this->cryptStub->doEncrypt( \json_encode( @@ -828,7 +835,7 @@ public function testRespondToAccessTokenRequestCodeChallengePlain() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => self::CODE_VERIFIER, 'code_challenge_method' => 'plain', ] @@ -848,7 +855,7 @@ public function testRespondToAccessTokenRequestCodeChallengeS256() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -891,7 +898,7 @@ public function testRespondToAccessTokenRequestCodeChallengeS256() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, 'code' => $this->cryptStub->doEncrypt( \json_encode( @@ -901,7 +908,7 @@ public function testRespondToAccessTokenRequestCodeChallengeS256() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => self::CODE_CHALLENGE, 'code_challenge_method' => 'S256', ] @@ -917,12 +924,83 @@ public function testRespondToAccessTokenRequestCodeChallengeS256() $this->assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); } + public function testPKCEDowngradeBlocked() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri(self::REDIRECT_URI); + $client->setConfidential(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new DateInterval('PT10M') + ); + + $grant->setClientRepository($clientRepositoryMock); + $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( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + '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, + ] + ) + ), + ] + ); + + $this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class); + $this->expectExceptionCode(3); + + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new DateInterval('PT10M')); + } + public function testRespondToAccessTokenRequestMissingRedirectUri() { $client = new ClientEntity(); $client->setIdentifier('foo'); $client->setConfidential(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -952,7 +1030,7 @@ public function testRespondToAccessTokenRequestMissingRedirectUri() 'auth_code_id' => \uniqid(), 'expire_time' => \time() + 3600, 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -1001,7 +1079,7 @@ public function testRespondToAccessTokenRequestRedirectUriMismatch() 'auth_code_id' => \uniqid(), 'expire_time' => \time() + 3600, 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -1017,7 +1095,7 @@ public function testRespondToAccessTokenRequestRedirectUriMismatch() public function testRespondToAccessTokenRequestMissingCode() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1048,7 +1126,7 @@ public function testRespondToAccessTokenRequestMissingCode() 'grant_type' => 'authorization_code', 'client_id' => 'foo', 'client_secret' => 'bar', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ); @@ -1062,7 +1140,7 @@ public function testRespondToAccessTokenRequestMissingCode() public function testRespondToAccessTokenRequestWithRefreshTokenInsteadOfAuthCode() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1088,7 +1166,7 @@ public function testRespondToAccessTokenRequestWithRefreshTokenInsteadOfAuthCode [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -1115,7 +1193,7 @@ public function testRespondToAccessTokenRequestWithRefreshTokenInsteadOfAuthCode public function testRespondToAccessTokenRequestWithAuthCodeNotAString() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1141,7 +1219,7 @@ public function testRespondToAccessTokenRequestWithAuthCodeNotAString() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => ['not', 'a', 'string'], ] ); @@ -1153,7 +1231,7 @@ public function testRespondToAccessTokenRequestWithAuthCodeNotAString() public function testRespondToAccessTokenRequestExpiredCode() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1179,7 +1257,7 @@ public function testRespondToAccessTokenRequestExpiredCode() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -1188,7 +1266,7 @@ public function testRespondToAccessTokenRequestExpiredCode() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -1207,7 +1285,7 @@ public function testRespondToAccessTokenRequestRevokedCode() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1243,7 +1321,7 @@ public function testRespondToAccessTokenRequestRevokedCode() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -1252,7 +1330,7 @@ public function testRespondToAccessTokenRequestRevokedCode() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -1271,7 +1349,7 @@ public function testRespondToAccessTokenRequestClientMismatch() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1304,7 +1382,7 @@ public function testRespondToAccessTokenRequestClientMismatch() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -1313,7 +1391,7 @@ public function testRespondToAccessTokenRequestClientMismatch() 'client_id' => 'bar', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -1332,7 +1410,7 @@ public function testRespondToAccessTokenRequestBadCodeEncryption() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1365,7 +1443,7 @@ public function testRespondToAccessTokenRequestBadCodeEncryption() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => 'sdfsfsd', ] ); @@ -1382,7 +1460,7 @@ public function testRespondToAccessTokenRequestBadCodeVerifierPlain() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1424,7 +1502,7 @@ public function testRespondToAccessTokenRequestBadCodeVerifierPlain() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => self::CODE_VERIFIER, 'code' => $this->cryptStub->doEncrypt( \json_encode( @@ -1434,7 +1512,7 @@ public function testRespondToAccessTokenRequestBadCodeVerifierPlain() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'foobar', 'code_challenge_method' => 'plain', ] @@ -1455,7 +1533,7 @@ public function testRespondToAccessTokenRequestBadCodeVerifierS256() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1497,7 +1575,7 @@ public function testRespondToAccessTokenRequestBadCodeVerifierS256() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'nope', 'code' => $this->cryptStub->doEncrypt( \json_encode( @@ -1507,7 +1585,7 @@ public function testRespondToAccessTokenRequestBadCodeVerifierS256() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'foobar', 'code_challenge_method' => 'S256', ] @@ -1528,7 +1606,7 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1570,7 +1648,7 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'dqX7C-RbqjHYtytmhGTigKdZCXfxq-+xbsk9_GxUcaE', // Malformed code. Contains `+`. 'code' => $this->cryptStub->doEncrypt( \json_encode( @@ -1580,7 +1658,7 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => self::CODE_CHALLENGE, 'code_challenge_method' => 'S256', ] @@ -1601,7 +1679,7 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1643,7 +1721,7 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_verifier' => 'dqX7C-RbqjHY', // Malformed code. Invalid length. 'code' => $this->cryptStub->doEncrypt( \json_encode( @@ -1653,7 +1731,7 @@ public function testRespondToAccessTokenRequestMalformedCodeVerifierS256WithInva 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'R7T1y1HPNFvs1WDCrx4lfoBS6KD2c71pr8OHvULjvv8', 'code_challenge_method' => 'S256', ] @@ -1674,7 +1752,7 @@ public function testRespondToAccessTokenRequestMissingCodeVerifier() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1716,7 +1794,7 @@ public function testRespondToAccessTokenRequestMissingCodeVerifier() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -1725,7 +1803,7 @@ public function testRespondToAccessTokenRequestMissingCodeVerifier() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code_challenge' => 'foobar', 'code_challenge_method' => 'plain', ] @@ -1744,9 +1822,12 @@ public function testRespondToAccessTokenRequestMissingCodeVerifier() public function testAuthCodeRepositoryUniqueConstraintCheck() { + $client = new ClientEntity(); + $client->setRedirectUri(self::REDIRECT_URI); + $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); - $authRequest->setClient(new ClientEntity()); + $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); @@ -1828,7 +1909,7 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1879,7 +1960,7 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -1888,7 +1969,7 @@ public function testRefreshTokenRepositoryUniqueConstraintCheck() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -1906,7 +1987,7 @@ public function testRefreshTokenRepositoryFailToPersist() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -1947,7 +2028,7 @@ public function testRefreshTokenRepositoryFailToPersist() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -1956,7 +2037,7 @@ public function testRefreshTokenRepositoryFailToPersist() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -1977,7 +2058,7 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop() { $client = new ClientEntity(); $client->setIdentifier('foo'); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -2018,7 +2099,7 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop() [ 'grant_type' => 'authorization_code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, 'code' => $this->cryptStub->doEncrypt( \json_encode( [ @@ -2027,7 +2108,7 @@ public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop() 'client_id' => 'foo', 'user_id' => 123, 'scopes' => ['foo'], - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ] ) ), @@ -2060,7 +2141,7 @@ public function testCompleteAuthorizationRequestNoUser() public function testPublicClientAuthCodeRequestRejectedWhenCodeChallengeRequiredButNotGiven() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -2082,7 +2163,7 @@ public function testPublicClientAuthCodeRequestRejectedWhenCodeChallengeRequired $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ]); $this->expectException(OAuthServerException::class); @@ -2094,7 +2175,7 @@ public function testPublicClientAuthCodeRequestRejectedWhenCodeChallengeRequired public function testUseValidRedirectUriIfScopeCheckFails() { $client = new ClientEntity(); - $client->setRedirectUri(['http://foo/bar', 'http://bar/foo']); + $client->setRedirectUri([self::REDIRECT_URI, 'http://bar/foo']); $client->setConfidential(); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); diff --git a/tests/Grant/ImplicitGrantTest.php b/tests/Grant/ImplicitGrantTest.php index 546450384..5f69242c7 100644 --- a/tests/Grant/ImplicitGrantTest.php +++ b/tests/Grant/ImplicitGrantTest.php @@ -25,6 +25,7 @@ class ImplicitGrantTest extends TestCase { const DEFAULT_SCOPE = 'basic'; + const REDIRECT_URI = 'https://foo/bar'; /** * CryptTrait stub @@ -79,7 +80,7 @@ public function testCanRespondToAuthorizationRequest() public function testValidateAuthorizationRequest() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -95,7 +96,7 @@ public function testValidateAuthorizationRequest() $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ]); $this->assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request)); @@ -104,7 +105,7 @@ public function testValidateAuthorizationRequest() public function testValidateAuthorizationRequestRedirectUriArray() { $client = new ClientEntity(); - $client->setRedirectUri(['http://foo/bar']); + $client->setRedirectUri([self::REDIRECT_URI]); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -120,7 +121,7 @@ public function testValidateAuthorizationRequestRedirectUriArray() $request = (new ServerRequest())->withQueryParams([ 'response_type' => 'code', 'client_id' => 'foo', - 'redirect_uri' => 'http://foo/bar', + 'redirect_uri' => self::REDIRECT_URI, ]); $this->assertInstanceOf(AuthorizationRequest::class, $grant->validateAuthorizationRequest($request)); @@ -163,7 +164,7 @@ public function testValidateAuthorizationRequestInvalidClientId() public function testValidateAuthorizationRequestBadRedirectUriString() { $client = new ClientEntity(); - $client->setRedirectUri('http://foo/bar'); + $client->setRedirectUri(self::REDIRECT_URI); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -185,7 +186,7 @@ public function testValidateAuthorizationRequestBadRedirectUriString() public function testValidateAuthorizationRequestBadRedirectUriArray() { $client = new ClientEntity(); - $client->setRedirectUri(['http://foo/bar']); + $client->setRedirectUri([self::REDIRECT_URI]); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -208,6 +209,7 @@ public function testCompleteAuthorizationRequest() { $client = new ClientEntity(); $client->setIdentifier('identifier'); + $client->setRedirectUri(self::REDIRECT_URI); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); @@ -235,9 +237,12 @@ public function testCompleteAuthorizationRequest() public function testCompleteAuthorizationRequestDenied() { + $client = new ClientEntity(); + $client->setRedirectUri(self::REDIRECT_URI); + $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(false); - $authRequest->setClient(new ClientEntity()); + $authRequest->setClient($client); $authRequest->setGrantTypeId('authorization_code'); $authRequest->setUser(new UserEntity()); @@ -263,6 +268,7 @@ public function testAccessTokenRepositoryUniqueConstraintCheck() { $client = new ClientEntity(); $client->setIdentifier('identifier'); + $client->setRedirectUri(self::REDIRECT_URI); $authRequest = new AuthorizationRequest(); $authRequest->setAuthorizationApproved(true); diff --git a/tests/Stubs/ScopeEntity.php b/tests/Stubs/ScopeEntity.php index 4e4a6bec5..4c93d91dc 100644 --- a/tests/Stubs/ScopeEntity.php +++ b/tests/Stubs/ScopeEntity.php @@ -9,6 +9,7 @@ class ScopeEntity implements ScopeEntityInterface { use EntityTrait; + #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->getIdentifier();