Merge pull request #1000 from filecage/master

Optional Refresh Tokens
This commit is contained in:
Andrew Millington 2019-05-05 09:48:53 +01:00 committed by GitHub
commit 382b6f5fbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 228 additions and 29 deletions

View File

@ -41,7 +41,6 @@ enabled:
- print_to_echo - print_to_echo
- short_array_syntax - short_array_syntax
- short_scalar_cast - short_scalar_cast
- simplified_null_return
- single_quote - single_quote
- spaces_cast - spaces_cast
- standardize_not_equal - standardize_not_equal

View File

@ -5,6 +5,8 @@ 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). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Changed
- RefreshTokenRepository can now return null, allowing refresh tokens to be optional. (PR #649)
## [7.3.3] - released 2019-03-29 ## [7.3.3] - released 2019-03-29
### Added ### Added

View File

@ -472,16 +472,21 @@ abstract class AbstractGrant implements GrantTypeInterface
* @throws OAuthServerException * @throws OAuthServerException
* @throws UniqueTokenIdentifierConstraintViolationException * @throws UniqueTokenIdentifierConstraintViolationException
* *
* @return RefreshTokenEntityInterface * @return RefreshTokenEntityInterface|null
*/ */
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken) protected function issueRefreshToken(AccessTokenEntityInterface $accessToken)
{ {
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
$refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
if ($refreshToken === null) {
return null;
}
$refreshToken->setExpiryDateTime((new DateTime())->add($this->refreshTokenTTL)); $refreshToken->setExpiryDateTime((new DateTime())->add($this->refreshTokenTTL));
$refreshToken->setAccessToken($accessToken); $refreshToken->setAccessToken($accessToken);
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
while ($maxGenerationAttempts-- > 0) { while ($maxGenerationAttempts-- > 0) {
$refreshToken->setIdentifier($this->generateUniqueIdentifier()); $refreshToken->setIdentifier($this->generateUniqueIdentifier());
try { try {

View File

@ -145,17 +145,18 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
} }
} }
// Issue and persist access + refresh tokens // Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes); $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes);
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
$refreshToken = $this->issueRefreshToken($accessToken); $refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); $responseType->setRefreshToken($refreshToken);
}
// Inject tokens into response type
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
// Revoke used auth code // Revoke used auth code
$this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id); $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id);

View File

@ -56,17 +56,18 @@ class PasswordGrant extends AbstractGrant
// Finalize the requested scopes // Finalize the requested scopes
$finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier()); $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
// Issue and persist new tokens // Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes); $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes);
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
$refreshToken = $this->issueRefreshToken($accessToken); $refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); $responseType->setRefreshToken($refreshToken);
}
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
return $responseType; return $responseType;
} }

View File

@ -63,17 +63,18 @@ class RefreshTokenGrant extends AbstractGrant
$this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']);
$this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']);
// Issue and persist new tokens // Issue and persist new access token
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes); $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes);
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$responseType->setAccessToken($accessToken);
// Issue and persist new refresh token if given
$refreshToken = $this->issueRefreshToken($accessToken); $refreshToken = $this->issueRefreshToken($accessToken);
// Send events to emitter if ($refreshToken !== null) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request)); $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request)); $responseType->setRefreshToken($refreshToken);
}
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
return $responseType; return $responseType;
} }

View File

@ -20,7 +20,7 @@ interface RefreshTokenRepositoryInterface extends RepositoryInterface
/** /**
* Creates a new refresh token * Creates a new refresh token
* *
* @return RefreshTokenEntityInterface * @return RefreshTokenEntityInterface|null
*/ */
public function getNewRefreshToken(); public function getNewRefreshToken();

View File

@ -346,6 +346,27 @@ class AbstractGrantTest extends TestCase
$this->assertEquals($accessToken, $refreshToken->getAccessToken()); $this->assertEquals($accessToken, $refreshToken->getAccessToken());
} }
public function testIssueNullRefreshToken()
{
$refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
$refreshTokenRepoMock
->expects($this->once())
->method('getNewRefreshToken')
->willReturn(null);
/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setRefreshTokenTTL(new \DateInterval('PT1M'));
$grantMock->setRefreshTokenRepository($refreshTokenRepoMock);
$abstractGrantReflection = new \ReflectionClass($grantMock);
$issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken');
$issueRefreshTokenMethod->setAccessible(true);
$accessToken = new AccessTokenEntity();
$this->assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken));
}
public function testIssueAccessToken() public function testIssueAccessToken()
{ {
$accessTokenRepoMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); $accessTokenRepoMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();

View File

@ -644,6 +644,73 @@ class AuthCodeGrantTest extends TestCase
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken()); $this->assertInstanceOf(RefreshTokenEntityInterface::class, $response->getRefreshToken());
} }
public function testRespondToAccessTokenRequestNullRefreshToken()
{
$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(null);
$grant = new AuthCodeGrant(
$this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(),
$refreshTokenRepositoryMock,
new \DateInterval('PT10M')
);
$grant->setClientRepository($clientRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setEncryptionKey($this->cryptStub->getKey());
$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',
]
)
),
]
);
/** @var StubResponseType $response */
$response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $response->getAccessToken());
$this->assertNull($response->getRefreshToken());
}
public function testRespondToAccessTokenRequestCodeChallengePlain() public function testRespondToAccessTokenRequestCodeChallengePlain()
{ {
$client = new ClientEntity(); $client = new ClientEntity();

View File

@ -78,6 +78,51 @@ class PasswordGrantTest extends TestCase
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); $this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken());
} }
public function testRespondToRequestNullRefreshToken()
{
$client = new ClientEntity();
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
$accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
$userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock();
$userEntity = new UserEntity();
$userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn($userEntity);
$scope = new ScopeEntity();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
$refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
$refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(null);
$grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock);
$grant->setClientRepository($clientRepositoryMock);
$grant->setAccessTokenRepository($accessTokenRepositoryMock);
$grant->setScopeRepository($scopeRepositoryMock);
$grant->setDefaultScope(self::DEFAULT_SCOPE);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody(
[
'client_id' => 'foo',
'client_secret' => 'bar',
'username' => 'foo',
'password' => 'bar',
]
);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken());
$this->assertNull($responseType->getRefreshToken());
}
/** /**
* @expectedException \League\OAuth2\Server\Exception\OAuthServerException * @expectedException \League\OAuth2\Server\Exception\OAuthServerException
*/ */

View File

@ -94,6 +94,63 @@ class RefreshTokenGrantTest extends TestCase
$this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken()); $this->assertInstanceOf(RefreshTokenEntityInterface::class, $responseType->getRefreshToken());
} }
public function testRespondToRequestNullRefreshToken()
{
$client = new ClientEntity();
$client->setIdentifier('foo');
$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('getNewRefreshToken')->willReturn(null);
$refreshTokenRepositoryMock->expects($this->never())->method('persistNewRefreshToken');
$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'));
$oldRefreshToken = $this->cryptStub->doEncrypt(
json_encode(
[
'client_id' => 'foo',
'refresh_token_id' => 'zyxwvu',
'access_token_id' => 'abcdef',
'scopes' => ['foo'],
'user_id' => 123,
'expire_time' => time() + 3600,
]
)
);
$serverRequest = new ServerRequest();
$serverRequest = $serverRequest->withParsedBody([
'client_id' => 'foo',
'client_secret' => 'bar',
'refresh_token' => $oldRefreshToken,
'scopes' => ['foo'],
]);
$responseType = new StubResponseType();
$grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M'));
$this->assertInstanceOf(AccessTokenEntityInterface::class, $responseType->getAccessToken());
$this->assertNull($responseType->getRefreshToken());
}
public function testRespondToReducedScopes() public function testRespondToReducedScopes()
{ {
$client = new ClientEntity(); $client = new ClientEntity();