diff --git a/src/League/OAuth2/Server/Grant/RefreshToken.php b/src/League/OAuth2/Server/Grant/RefreshToken.php index 78e95269..99d759b0 100644 --- a/src/League/OAuth2/Server/Grant/RefreshToken.php +++ b/src/League/OAuth2/Server/Grant/RefreshToken.php @@ -54,6 +54,12 @@ class RefreshToken implements GrantTypeInterface { */ protected $refreshTokenTTL = 604800; + /** + * Rotate refresh tokens + * @var boolean + */ + protected $rotateRefreshTokens = false; + /** * Constructor * @param Authorization $authServer Authorization server instance @@ -111,6 +117,16 @@ class RefreshToken implements GrantTypeInterface { return $this->refreshTokenTTL; } + /** + * When a new access is token, expire the refresh token used and issue a new one. + * @param boolean $rotateRefreshTokens Set to true to enable (default = false) + * @return void + */ + public function rotateRefreshTokens($rotateRefreshTokens = false) + { + $this->rotateRefreshTokens = $rotateRefreshTokens; + } + /** * Complete the refresh token grant * @param null|array $inputParams @@ -160,24 +176,32 @@ class RefreshToken implements GrantTypeInterface { $accessTokenExpiresIn = ($this->accessTokenTTL !== null) ? $this->accessTokenTTL : $this->authServer->getAccessTokenTTL(); $accessTokenExpires = time() + $accessTokenExpiresIn; - // Generate a new refresh token - $refreshToken = SecureKey::make(); - $refreshTokenExpires = time() + $this->getRefreshTokenTTL(); - - // Revoke the old refresh token - $this->authServer->getStorage('session')->removeRefreshToken($authParams['refresh_token']); - // Associate the new access token with the session $newAccessTokenId = $this->authServer->getStorage('session')->associateAccessToken($accessTokenDetails['session_id'], $accessToken, $accessTokenExpires); - // There isn't a request for reduced scopes so assign the original ones + if ($this->rotateRefreshTokens === true) { + + // Generate a new refresh token + $refreshToken = SecureKey::make(); + $refreshTokenExpires = time() + $this->getRefreshTokenTTL(); + + // Revoke the old refresh token + $this->authServer->getStorage('session')->removeRefreshToken($authParams['refresh_token']); + + // Associate the new refresh token with the new access token + $this->authServer->getStorage('session')->associateRefreshToken($newAccessTokenId, $refreshToken, $refreshTokenExpires, $authParams['client_id']); + } + + // There isn't a request for reduced scopes so assign the original ones (or we're not rotating scopes) if ( ! isset($authParams['scope'])) { + foreach ($scopes as $scope) { $this->authServer->getStorage('session')->associateScope($newAccessTokenId, $scope['id']); } - } else { - // The request is asking for reduced scopes + } elseif ( isset($authParams['scope']) && $this->rotateRefreshTokens === true) { + + // The request is asking for reduced scopes and rotate tokens is enabled $reqestedScopes = explode($this->authServer->getScopeDelimeter(), $authParams['scope']); for ($i = 0; $i < count($reqestedScopes); $i++) { @@ -202,16 +226,18 @@ class RefreshToken implements GrantTypeInterface { } } - // Associate the new refresh token with the new access token - $this->authServer->getStorage('session')->associateRefreshToken($newAccessTokenId, $refreshToken, $refreshTokenExpires, $authParams['client_id']); - - return array( + $response = array( 'access_token' => $accessToken, - 'refresh_token' => $refreshToken, 'token_type' => 'bearer', 'expires' => $accessTokenExpires, 'expires_in' => $accessTokenExpiresIn ); + + if ($this->rotateRefreshTokens === true) { + $response['refresh_token'] = $refreshToken; + } + + return $response; } } diff --git a/tests/authorization/RefreshTokenTest.php b/tests/authorization/RefreshTokenTest.php index 3f32acae..12dad385 100644 --- a/tests/authorization/RefreshTokenTest.php +++ b/tests/authorization/RefreshTokenTest.php @@ -204,7 +204,6 @@ class Refresh_Token_test extends PHPUnit_Framework_TestCase $this->assertArrayHasKey('token_type', $v); $this->assertArrayHasKey('expires', $v); $this->assertArrayHasKey('expires_in', $v); - $this->assertArrayHasKey('refresh_token', $v); $this->assertEquals($a->getAccessTokenTTL(), $v['expires_in']); $this->assertEquals(time()+$a->getAccessTokenTTL(), $v['expires']); @@ -240,6 +239,48 @@ class Refresh_Token_test extends PHPUnit_Framework_TestCase 'refresh_token' => 'abcdef', )); + $this->assertArrayHasKey('access_token', $v); + $this->assertArrayHasKey('token_type', $v); + $this->assertArrayHasKey('expires', $v); + $this->assertArrayHasKey('expires_in', $v); + + $this->assertEquals($a->getAccessTokenTTL(), $v['expires_in']); + $this->assertEquals(time()+$a->getAccessTokenTTL(), $v['expires']); + } + + public function test_issueAccessToken_refreshTokenGrant_rotateTokens() + { + $this->client->shouldReceive('getClient')->andReturn(array( + 'client_id' => 1234, + 'client_secret' => 5678, + 'redirect_uri' => 'http://foo/redirect', + 'name' => 'Example Client' + )); + + $this->session->shouldReceive('validateRefreshToken')->andReturn(1); + $this->session->shouldReceive('validateAuthCode')->andReturn(1); + $this->session->shouldReceive('updateSession')->andReturn(null); + $this->session->shouldReceive('updateRefreshToken')->andReturn(null); + $this->session->shouldReceive('getAccessToken')->andReturn(null); + $this->session->shouldReceive('getScopes')->andReturn(array('id' => 1)); + $this->session->shouldReceive('associateAccessToken')->andReturn(1); + $this->session->shouldReceive('associateRefreshToken')->andReturn(1); + $this->session->shouldReceive('removeRefreshToken')->andReturn(1); + $this->session->shouldReceive('associateScope')->andReturn(null); + + $a = $this->returnDefault(); + + $rt = new League\OAuth2\Server\Grant\RefreshToken($a); + $rt->rotateRefreshTokens(true); + $a->addGrantType($rt); + + $v = $a->issueAccessToken(array( + 'grant_type' => 'refresh_token', + 'client_id' => 1234, + 'client_secret' => 5678, + 'refresh_token' => 'abcdef', + )); + $this->assertArrayHasKey('access_token', $v); $this->assertArrayHasKey('token_type', $v); $this->assertArrayHasKey('expires', $v); @@ -286,7 +327,6 @@ class Refresh_Token_test extends PHPUnit_Framework_TestCase $this->assertArrayHasKey('token_type', $v); $this->assertArrayHasKey('expires', $v); $this->assertArrayHasKey('expires_in', $v); - $this->assertArrayHasKey('refresh_token', $v); $this->assertNotEquals($a->getAccessTokenTTL(), $v['expires_in']); $this->assertNotEquals(time()+$a->getAccessTokenTTL(), $v['expires']); @@ -318,6 +358,7 @@ class Refresh_Token_test extends PHPUnit_Framework_TestCase $a = $this->returnDefault(); $grant = new League\OAuth2\Server\Grant\RefreshToken($a); $grant->setAccessTokenTTL(30); + $grant->rotateRefreshTokens(true); $a->addGrantType($grant); $v = $a->issueAccessToken(array( @@ -368,6 +409,7 @@ class Refresh_Token_test extends PHPUnit_Framework_TestCase $a = $this->returnDefault(); $grant = new League\OAuth2\Server\Grant\RefreshToken($a); $grant->setAccessTokenTTL(30); + $grant->rotateRefreshTokens(true); $a->addGrantType($grant); $a->issueAccessToken(array(