diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index 8b0b2815e..1b6d593a7 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -79,6 +79,11 @@ class AuthorizationServer implements EmitterAwareInterface */ private $defaultScope = ''; + /** + * @var bool + */ + private $revokeRefreshTokens = true; + /** * New server instance. * @@ -136,6 +141,7 @@ public function enableGrantType(GrantTypeInterface $grantType, DateInterval $acc $grantType->setPrivateKey($this->privateKey); $grantType->setEmitter($this->getEmitter()); $grantType->setEncryptionKey($this->encryptionKey); + $grantType->setRevokeRefreshTokens($this->revokeRefreshTokens); $this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType; $this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] = $accessTokenTTL; @@ -233,4 +239,14 @@ public function setDefaultScope($defaultScope) { $this->defaultScope = $defaultScope; } + + /** + * Sets wether to revoke refresh tokens or not (for all grant types). + * + * @param bool $revokeRefreshTokens + */ + public function setRevokeRefreshTokens(bool $revokeRefreshTokens): void + { + $this->revokeRefreshTokens = $revokeRefreshTokens; + } } diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index fb9b1a509..8192da1a6 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -93,6 +93,11 @@ abstract class AbstractGrant implements GrantTypeInterface */ protected $defaultScope; + /** + * @var bool + */ + protected $revokeRefreshTokens; + /** * @param ClientRepositoryInterface $clientRepository */ @@ -167,6 +172,14 @@ public function setDefaultScope($scope) $this->defaultScope = $scope; } + /** + * @param bool $revokeRefreshTokens + */ + public function setRevokeRefreshTokens(bool $revokeRefreshTokens) + { + $this->revokeRefreshTokens = $revokeRefreshTokens; + } + /** * Validate the client. * @@ -178,7 +191,7 @@ public function setDefaultScope($scope) */ protected function validateClient(ServerRequestInterface $request) { - list($clientId, $clientSecret) = $this->getClientCredentials($request); + [$clientId, $clientSecret] = $this->getClientCredentials($request); if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); @@ -239,7 +252,7 @@ protected function getClientEntityOrFail($clientId, ServerRequestInterface $requ */ protected function getClientCredentials(ServerRequestInterface $request) { - list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request); + [$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request); $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser); diff --git a/src/Grant/RefreshTokenGrant.php b/src/Grant/RefreshTokenGrant.php index 8759aab4b..2dedf15c3 100644 --- a/src/Grant/RefreshTokenGrant.php +++ b/src/Grant/RefreshTokenGrant.php @@ -65,7 +65,9 @@ public function respondToAccessTokenRequest( // Expire old tokens $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); - $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); + if ($this->revokeRefreshTokens) { + $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); + } // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes); @@ -73,11 +75,13 @@ public function respondToAccessTokenRequest( $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given - $refreshToken = $this->issueRefreshToken($accessToken); + if ($this->revokeRefreshTokens) { + $refreshToken = $this->issueRefreshToken($accessToken); - if ($refreshToken !== null) { - $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); - $responseType->setRefreshToken($refreshToken); + if ($refreshToken !== null) { + $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); + $responseType->setRefreshToken($refreshToken); + } } return $responseType; diff --git a/tests/Grant/RefreshTokenGrantTest.php b/tests/Grant/RefreshTokenGrantTest.php index 48e81f619..090919c61 100644 --- a/tests/Grant/RefreshTokenGrantTest.php +++ b/tests/Grant/RefreshTokenGrantTest.php @@ -18,6 +18,7 @@ use LeagueTests\Stubs\RefreshTokenEntity; use LeagueTests\Stubs\ScopeEntity; use LeagueTests\Stubs\StubResponseType; +use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; class RefreshTokenGrantTest extends TestCase @@ -68,6 +69,7 @@ public function testRespondToRequest() $grant->setAccessTokenRepository($accessTokenRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $grant->setRevokeRefreshTokens(true); $oldRefreshToken = $this->cryptStub->doEncrypt( \json_encode( @@ -181,6 +183,7 @@ public function testRespondToReducedScopes() $grant->setScopeRepository($scopeRepositoryMock); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $grant->setRevokeRefreshTokens(true); $oldRefreshToken = $this->cryptStub->doEncrypt( \json_encode( @@ -467,4 +470,118 @@ public function testRespondToRequestRevokedToken() $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } + + public function testRevokedRefreshToken() + { + $refreshTokenId = 'foo'; + + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeEntity = new ScopeEntity(); + $scopeEntity->setIdentifier('foo'); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->expects($this->once())->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('isRefreshTokenRevoked') + ->will($this->onConsecutiveCalls(false, true)); + $refreshTokenRepositoryMock->expects($this->once())->method('revokeRefreshToken')->with($this->equalTo($refreshTokenId)); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + \json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => $refreshTokenId, + 'access_token_id' => 'abcdef', + 'scopes' => ['foo'], + 'user_id' => 123, + 'expire_time' => \time() + 3600, + ] + ) + ); + + $serverRequest = (new ServerRequest())->withParsedBody([ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + 'scope' => ['foo'], + ]); + + $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->setRevokeRefreshTokens(true); + $grant->respondToAccessTokenRequest($serverRequest, new StubResponseType(), new DateInterval('PT5M')); + + Assert::assertTrue($refreshTokenRepositoryMock->isRefreshTokenRevoked($refreshTokenId)); + } + + public function testUnrevokedRefreshToken() + { + $refreshTokenId = 'foo'; + + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeEntity = new ScopeEntity(); + $scopeEntity->setIdentifier('foo'); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->expects($this->once())->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('isRefreshTokenRevoked')->willReturn(false); + $refreshTokenRepositoryMock->expects($this->never())->method('revokeRefreshToken'); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + \json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => $refreshTokenId, + 'access_token_id' => 'abcdef', + 'scopes' => ['foo'], + 'user_id' => 123, + 'expire_time' => \time() + 3600, + ] + ) + ); + + $serverRequest = (new ServerRequest())->withParsedBody([ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + 'scope' => ['foo'], + ]); + + $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->respondToAccessTokenRequest($serverRequest, new StubResponseType(), new DateInterval('PT5M')); + + Assert::assertFalse($refreshTokenRepositoryMock->isRefreshTokenRevoked($refreshTokenId)); + } }