From 8e8aed1a505b2be954632a6cbd2ef89af8799a4e Mon Sep 17 00:00:00 2001 From: Alex Bilbie Date: Fri, 6 May 2016 15:23:16 +0100 Subject: [PATCH] Implemented RFC7636. Fixes #574 --- src/Exception/OAuthServerException.php | 20 + src/Grant/AuthCodeGrant.php | 78 +++- src/RequestTypes/AuthorizationRequest.php | 44 ++ tests/Grant/AuthCodeGrantTest.php | 477 +++++++++++++++++++++- 4 files changed, 611 insertions(+), 8 deletions(-) diff --git a/src/Exception/OAuthServerException.php b/src/Exception/OAuthServerException.php index 2f698341..4a6e1832 100644 --- a/src/Exception/OAuthServerException.php +++ b/src/Exception/OAuthServerException.php @@ -172,6 +172,26 @@ class OAuthServerException extends \Exception ); } + /** + * Invalid grant. + * + * @param string $hint + * + * @return static + */ + public static function invalidGrant($hint = '') + { + return new static( + 'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token ' + . 'is invalid, expired, revoked, does not match the redirection URI used in the authorization request, ' + . 'or was issued to another client.', + 10, + 'invalid_grant', + 400, + $hint + ); + } + /** * @return string */ diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index 9216e167..5f736709 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -29,6 +29,11 @@ class AuthCodeGrant extends AbstractAuthorizeGrant */ private $authCodeTTL; + /** + * @var bool + */ + private $enableCodeExchangeProof = false; + /** * @param \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface $authCodeRepository * @param \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface $refreshTokenRepository @@ -45,6 +50,11 @@ class AuthCodeGrant extends AbstractAuthorizeGrant $this->refreshTokenTTL = new \DateInterval('P1M'); } + public function enableCodeExchangeProof() + { + $this->enableCodeExchangeProof = true; + } + /** * Respond to an access token request. * @@ -118,6 +128,42 @@ class AuthCodeGrant extends AbstractAuthorizeGrant throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code'); } + // Validate code challenge + if ($this->enableCodeExchangeProof === true) { + $codeVerifier = $this->getRequestParameter('code_verifier', $request, null); + if ($codeVerifier === null) { + throw OAuthServerException::invalidRequest('code_verifier'); + } + + switch ($authCodePayload->code_challenge_method) { + case 'plain': + if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) { + throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.'); + } + + break; + case 'S256': + if ( + hash_equals( + urlencode(base64_encode(hash('sha256', $codeVerifier))), + $authCodePayload->code_challenge + ) === false + ) { + throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.'); + } + // @codeCoverageIgnoreStart + break; + default: + throw OAuthServerException::serverError( + sprintf( + 'Unsupported code challenge method `%s`', + $authCodePayload->code_challenge_method + ) + ); + // @codeCoverageIgnoreEnd + } + } + // Issue and persist access + refresh tokens $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes); $refreshToken = $this->issueRefreshToken($accessToken); @@ -213,6 +259,24 @@ class AuthCodeGrant extends AbstractAuthorizeGrant $authorizationRequest->setState($stateParameter); $authorizationRequest->setScopes($scopes); + if ($this->enableCodeExchangeProof === true) { + $codeChallenge = $this->getQueryStringParameter('code_challenge', $request); + if ($codeChallenge === null) { + throw OAuthServerException::invalidRequest('code_challenge'); + } + + $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain'); + if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) { + throw OAuthServerException::invalidRequest( + 'code_challenge_method', + 'Code challenge method must be `plain` or `S256`' + ); + } + + $authorizationRequest->setCodeChallenge($codeChallenge); + $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod); + } + return $authorizationRequest; } @@ -249,12 +313,14 @@ class AuthCodeGrant extends AbstractAuthorizeGrant 'code' => $this->encrypt( json_encode( [ - 'client_id' => $authCode->getClient()->getIdentifier(), - 'redirect_uri' => $authCode->getRedirectUri(), - 'auth_code_id' => $authCode->getIdentifier(), - 'scopes' => $authCode->getScopes(), - 'user_id' => $authCode->getUserIdentifier(), - 'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'), + 'client_id' => $authCode->getClient()->getIdentifier(), + 'redirect_uri' => $authCode->getRedirectUri(), + 'auth_code_id' => $authCode->getIdentifier(), + 'scopes' => $authCode->getScopes(), + 'user_id' => $authCode->getUserIdentifier(), + 'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'), + 'code_challenge' => $authorizationRequest->getCodeChallenge(), + 'code_challenge_method ' => $authorizationRequest->getCodeChallengeMethod(), ] ) ), diff --git a/src/RequestTypes/AuthorizationRequest.php b/src/RequestTypes/AuthorizationRequest.php index eaf78e39..2a5fcd5f 100644 --- a/src/RequestTypes/AuthorizationRequest.php +++ b/src/RequestTypes/AuthorizationRequest.php @@ -64,6 +64,18 @@ class AuthorizationRequest */ protected $state; + /** + * The code challenge (if provided) + * @var string + */ + protected $codeChallenge; + + /** + * The code challenge method (if provided) + * @var string + */ + protected $codeChallengeMethod; + /** * @return string */ @@ -175,4 +187,36 @@ class AuthorizationRequest { $this->state = $state; } + + /** + * @return string + */ + public function getCodeChallenge() + { + return $this->codeChallenge; + } + + /** + * @param string $codeChallenge + */ + public function setCodeChallenge($codeChallenge) + { + $this->codeChallenge = $codeChallenge; + } + + /** + * @return string + */ + public function getCodeChallengeMethod() + { + return $this->codeChallengeMethod; + } + + /** + * @param string $codeChallengeMethod + */ + public function setCodeChallengeMethod($codeChallengeMethod) + { + $this->codeChallengeMethod = $codeChallengeMethod; + } } diff --git a/tests/Grant/AuthCodeGrantTest.php b/tests/Grant/AuthCodeGrantTest.php index 3783d349..09ea03e6 100644 --- a/tests/Grant/AuthCodeGrantTest.php +++ b/tests/Grant/AuthCodeGrantTest.php @@ -27,7 +27,7 @@ use Zend\Diactoros\ServerRequest; class AuthCodeGrantTest extends \PHPUnit_Framework_TestCase { /** - * CryptTrait stub + * @var CryptTraitStub */ protected $cryptStub; @@ -136,6 +136,41 @@ class AuthCodeGrantTest extends \PHPUnit_Framework_TestCase $this->assertTrue($grant->validateAuthorizationRequest($request) instanceof AuthorizationRequest); } + + public function testValidateAuthorizationRequestCodeChallenge() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMock(AuthCodeRepositoryInterface::class), + $this->getMock(RefreshTokenRepositoryInterface::class), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => 'FOOBAR', + ] + ); + + $this->assertTrue($grant->validateAuthorizationRequest($request) instanceof AuthorizationRequest); + } + /** * @expectedException \League\OAuth2\Server\Exception\OAuthServerException * @expectedExceptionCode 3 @@ -272,6 +307,82 @@ class AuthCodeGrantTest extends \PHPUnit_Framework_TestCase $grant->validateAuthorizationRequest($request); } + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testValidateAuthorizationRequestMissingCodeChallenge() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMock(AuthCodeRepositoryInterface::class), + $this->getMock(RefreshTokenRepositoryInterface::class), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testValidateAuthorizationRequestInvalidCodeChallengeMethod() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMock(AuthCodeRepositoryInterface::class), + $this->getMock(RefreshTokenRepositoryInterface::class), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'foo', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + public function testCompleteAuthorizationRequest() { $authRequest = new AuthorizationRequest(); @@ -390,6 +501,150 @@ class AuthCodeGrantTest extends \PHPUnit_Framework_TestCase $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); } + public function testRespondToAccessTokenRequestCodeChallengePlain() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $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->getMock(AuthCodeRepositoryInterface::class), + $this->getMock(RefreshTokenRepositoryInterface::class), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + $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' => 'http://foo/bar', + 'code_verifier' => 'foobar', + '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', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'plain', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertTrue($response->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + public function testRespondToAccessTokenRequestCodeChallengeS256() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $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->getMock(AuthCodeRepositoryInterface::class), + $this->getMock(RefreshTokenRepositoryInterface::class), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + $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' => 'http://foo/bar', + 'code_verifier' => 'foobar', + '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', + 'code_challenge' => urlencode(base64_encode(hash('sha256', 'foobar'))), + 'code_challenge_method' => 'S256', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertTrue($response->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + /** * @expectedException \League\OAuth2\Server\Exception\OAuthServerException * @expectedExceptionCode 3 @@ -710,4 +965,222 @@ class AuthCodeGrantTest extends \PHPUnit_Framework_TestCase $this->assertEquals($e->getHint(), 'Cannot decrypt the authorization code'); } } -} + + public function testRespondToAccessTokenRequestBadCodeVerifierPlain() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $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->getMock(AuthCodeRepositoryInterface::class), + $this->getMock(RefreshTokenRepositoryInterface::class), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + $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' => 'http://foo/bar', + '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' => 'http://foo/bar', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'plain', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Failed to verify `code_verifier`.'); + } + } + + public function testRespondToAccessTokenRequestBadCodeVerifierS256() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $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->getMock(AuthCodeRepositoryInterface::class), + $this->getMock(RefreshTokenRepositoryInterface::class), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + $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' => 'http://foo/bar', + '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' => 'http://foo/bar', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'S256', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Failed to verify `code_verifier`.'); + } + } + + public function testRespondToAccessTokenRequestMissingCodeVerifier() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $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->getMock(AuthCodeRepositoryInterface::class), + $this->getMock(RefreshTokenRepositoryInterface::class), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + $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' => 'http://foo/bar', + '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', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'plain', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Check the `code_verifier` parameter'); + } + } +} \ No newline at end of file