Fix Access Token reissue with

Rotate Refresh Token policy active

If user configured his/her oauth2 client
with "Rotate Refresh Token Policy" active
once emmited access token re issuing
that newly access token create was invalid.

Change-Id: I037c8b561dc5b720c71ccba7d8e2a081fb9783e5
This commit is contained in:
Sebastian Marcet 2017-10-17 13:19:18 -03:00
parent d83b763c2c
commit 07294e97ac
6 changed files with 95 additions and 70 deletions

View File

@ -30,7 +30,7 @@ class RefreshToken extends BaseModelEloquent {
private $friendly_scopes;
protected $fillable = array('value', 'from_ip', 'lifetime','scope','audience','void','created_at','updated_at','client_id');
protected $fillable = ['value', 'from_ip', 'lifetime','scope','audience','void','created_at','updated_at','client_id'];
public function access_tokens()
{

View File

@ -23,7 +23,6 @@ use jwt\JWTClaim;
use OAuth2\Models\AccessToken;
use Models\OAuth2\AccessToken as AccessTokenDB;
use Models\OAuth2\RefreshToken as RefreshTokenDB;
use Models\OAuth2\ResourceServer;
use OAuth2\Builders\IdTokenBuilder;
use OAuth2\Exceptions\AbsentClientException;
use OAuth2\Exceptions\AbsentCurrentUserException;
@ -415,21 +414,10 @@ final class TokenService implements ITokenService
)
);
$cache_service = $this->cache_service;
$client_service = $this->client_service;
$auth_service = $this->auth_service;
$client_repository = $this->client_repository;
$access_token_repository = $this->access_token_repository;
return $this->tx_service->transaction(function () use (
$auth_code,
$redirect_uri,
$access_token,
$cache_service,
$client_service,
$auth_service,
$client_repository,
$access_token_repository
$access_token
) {
$value = $access_token->getValue();
@ -437,8 +425,8 @@ final class TokenService implements ITokenService
//oauth2 client id
$client_id = $access_token->getClientId();
$user_id = $access_token->getUserId();
$client = $client_repository->getClientById($client_id);
$user = $auth_service->getUserById($user_id);
$client = $this->client_repository->getClientById($client_id);
$user = $this->auth_service->getUserById($user_id);
// TODO; move to a factory
@ -458,7 +446,7 @@ final class TokenService implements ITokenService
$access_token_db->user()->associate($user);
$access_token_repository->add($access_token_db);
$this->access_token_repository->add($access_token_db);
//check if use refresh tokens...
Log::debug
@ -507,9 +495,9 @@ final class TokenService implements ITokenService
$this->storesAccessTokenOnCache($access_token);
//stores brand new access token hash value on a set by client id...
$cache_service->addMemberSet($client_id . TokenService::ClientAccessTokenPrefixList, $hashed_value);
$this->cache_service->addMemberSet($client_id . TokenService::ClientAccessTokenPrefixList, $hashed_value);
$cache_service->incCounter
$this->cache_service->incCounter
(
$client_id . TokenService::ClientAccessTokensQty,
TokenService::ClientAccessTokensQtyLifetime
@ -715,11 +703,26 @@ final class TokenService implements ITokenService
});
}
/**
* @param AccessToken $access_token
* @return bool
*/
private function clearAccessTokenOnCache(AccessToken $access_token){
$value = $access_token->getValue();
$hashed_value = Hash::compute('sha256', $value);
if ($this->cache_service->exists($hashed_value)) {
$this->cache_service->delete($hashed_value);
return true;
}
return false;
}
/**
* @param AccessToken $access_token
* @throws InvalidAccessTokenException
*/
public function storesAccessTokenOnCache(AccessToken $access_token)
private function storesAccessTokenOnCache(AccessToken $access_token)
{
//stores in REDIS
@ -751,11 +754,24 @@ final class TokenService implements ITokenService
], intval($access_token->getLifetime()));
}
/**
* @param AccessTokenDB $access_token
* @return bool
*/
private function clearAccessTokenDBOnCache(AccessTokenDB $access_token){
if ($this->cache_service->exists($access_token->value)) {
$this->cache_service->delete($access_token->value);
return true;
}
return false;
}
/**
* @param AccessTokenDB $access_token
* @throws InvalidAccessTokenException
*/
public function storeAccessTokenDBOnCache(AccessTokenDB $access_token)
private function storeAccessTokenDBOnCache(AccessTokenDB $access_token)
{
//stores in Cache
@ -796,18 +812,10 @@ final class TokenService implements ITokenService
*/
public function getAccessToken($value, $is_hashed = false)
{
$cache_service = $this->cache_service;
$lock_manager_service = $this->lock_manager_service;
$configuration_service = $this->configuration_service;
$access_token_repository = $this->access_token_repository;
return $this->tx_service->transaction(function () use (
$value,
$is_hashed,
$cache_service,
$lock_manager_service,
$access_token_repository,
$configuration_service
$is_hashed
) {
//hash the given value, bc tokens values are stored hashed on DB
$hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value;
@ -816,11 +824,11 @@ final class TokenService implements ITokenService
try
{
// check cache ...
if (!$cache_service->exists($hashed_value))
if (!$this->cache_service->exists($hashed_value))
{
$lock_manager_service->lock('lock.get.accesstoken.' . $hashed_value, function() use($value, $hashed_value, $access_token_repository){
$this->lock_manager_service->lock('lock.get.accesstoken.' . $hashed_value, function() use($value, $hashed_value){
// check on DB...
$access_token_db = $access_token_repository->getByValue($hashed_value);
$access_token_db = $this->access_token_repository->getByValue($hashed_value);
if (is_null($access_token_db))
{
if($this->isAccessTokenRevoked($hashed_value))
@ -847,8 +855,7 @@ final class TokenService implements ITokenService
});
}
$cache_values = $cache_service->getHash($hashed_value, array
(
$cache_values = $this->cache_service->getHash($hashed_value,[
'user_id',
'client_id',
'scope',
@ -858,7 +865,7 @@ final class TokenService implements ITokenService
'from_ip',
'audience',
'refresh_token'
));
]);
// reload auth code ...
$auth_code = AuthorizationCode::load
@ -870,7 +877,7 @@ final class TokenService implements ITokenService
$cache_values['audience'],
null,
null,
$configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'),
$this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'),
$cache_values['from_ip'],
$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,
$approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
@ -887,6 +894,7 @@ final class TokenService implements ITokenService
$cache_values['issued'],
$cache_values['lifetime']
);
$refresh_token_value = $cache_values['refresh_token'];
if (!empty($refresh_token_value)) {
@ -925,9 +933,10 @@ final class TokenService implements ITokenService
/**
* Creates a new refresh token and associate it with given access token
* @param AccessToken $access_token
* @param boolean $refresh_cache
* @return RefreshToken
*/
public function createRefreshToken(AccessToken &$access_token)
public function createRefreshToken(AccessToken &$access_token, $refresh_cache = false)
{
$refresh_token = $this->refresh_token_generator->generate(
RefreshToken::create(
@ -936,28 +945,18 @@ final class TokenService implements ITokenService
)
);
$client_repository = $this->client_repository;
$auth_service = $this->auth_service;
$cache_service = $this->cache_service;
$access_token_repository = $this->access_token_repository;
$refresh_token_repository = $this->refresh_token_repository;
return $this->tx_service->transaction(function () use (
$refresh_token,
$access_token,
$client_repository,
$auth_service,
$cache_service,
$access_token_repository,
$refresh_token_repository
$refresh_cache
) {
$value = $refresh_token->getValue();
//hash the given value, bc tokens values are stored hashed on DB
$hashed_value = Hash::compute('sha256', $value);
$client_id = $refresh_token->getClientId();
$user_id = $refresh_token->getUserId();
$client = $client_repository->getClientById($client_id);
$user = $auth_service->getUserById($user_id);
$client = $this->client_repository->getClientById($client_id);
$user = $this->auth_service->getUserById($user_id);
// todo: move to a factory
$refresh_token_db = new RefreshTokenDB (
@ -972,16 +971,23 @@ final class TokenService implements ITokenService
$refresh_token_db->client()->associate($client);
$refresh_token_db->user()->associate($user);
$refresh_token_repository->add($refresh_token_db);
$this->refresh_token_repository->add($refresh_token_db);
//associate current access token to refresh token on DB
$access_token_db = $this->access_token_repository->getByValue(Hash::compute('sha256', $access_token->getValue()));
$access_token_db->refresh_token()->associate($refresh_token_db);
$access_token_repository->add($access_token_db);
$this->access_token_repository->add($access_token_db);
$access_token->setRefreshToken($refresh_token);
// bc refresh token could change
if($refresh_cache) {
if($this->clearAccessTokenOnCache($access_token))
$this->storesAccessTokenOnCache($access_token);
if($this->clearAccessTokenDBOnCache($access_token_db))
$this->storeAccessTokenDBOnCache($access_token_db);
}
$cache_service->incCounter
$this->cache_service->incCounter
(
$client_id . TokenService::ClientRefreshTokensQty,
TokenService::ClientRefreshTokensQtyLifetime

View File

@ -159,9 +159,8 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
);
}
$access_token = $this->token_service->createAccessTokenFromRefreshToken($refresh_token, $scope);
$new_refresh_token = null;
$access_token = $this->token_service->createAccessTokenFromRefreshToken($refresh_token, $scope);
/*
* the authorization server could employ refresh token
* rotation in which a new refresh token is issued with every access
@ -174,7 +173,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
if ($this->current_client->useRotateRefreshTokenPolicy())
{
$this->token_service->invalidateRefreshToken($refresh_token_value);
$new_refresh_token = $this->token_service->createRefreshToken($access_token);
$new_refresh_token = $this->token_service->createRefreshToken($access_token, true);
}
$response = new OAuth2AccessTokenResponse

View File

@ -43,7 +43,7 @@ class RefreshToken extends Token {
}
public static function create(AccessToken $access_token, $lifetime = 0){
$instance = new self();
$instance = new self();
$instance->scope = $access_token->getScope();
$instance->user_id = $access_token->getUserId();
$instance->client_id = $access_token->getClientId();

View File

@ -123,9 +123,10 @@ interface ITokenService {
/**
* Creates a new refresh token and associate it with given access token
* @param AccessToken $access_token
* @param boolean $refresh_cache
* @return RefreshToken
*/
public function createRefreshToken(AccessToken &$access_token);
public function createRefreshToken(AccessToken &$access_token, $refresh_cache = false);
/**
* Get a refresh token by its value
@ -137,7 +138,6 @@ interface ITokenService {
*/
public function getRefreshToken($value, $is_hashed = false);
/**
* Revokes all related tokens to a specific auth code
* @param $auth_code Authorization Code
@ -181,7 +181,6 @@ interface ITokenService {
*/
public function invalidateRefreshToken($value, $is_hashed = false);
/**
* Revokes a give refresh token and all related access tokens
* @param $value

View File

@ -1,5 +1,16 @@
<?php
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Auth\User;
use Illuminate\Support\Facades\App;
use OAuth2\OAuth2Protocol;
@ -11,7 +22,7 @@ use Illuminate\Support\Facades\Session;
* Class OAuth2ProtocolTest
* Test Suite for OAuth2 Protocol
*/
class OAuth2ProtocolTest extends OpenStackIDBaseTest
final class OAuth2ProtocolTest extends OpenStackIDBaseTest
{
private $current_realm;
@ -104,7 +115,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest
}
/**
* Get Auth Code Test
*/
@ -365,7 +375,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
array(),
@ -518,7 +527,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
array(),
@ -616,7 +624,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest
$output = array();
parse_str($query, $output);
//do get auth token...
$params = array(
'code' => $output['code'],
@ -624,7 +631,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
array(),
@ -645,7 +651,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest
$this->assertTrue(!empty($access_token));
$this->assertTrue(!empty($refresh_token));
$params = array(
'refresh_token' => $refresh_token,
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_RefreshToken,
@ -667,12 +672,28 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest
$response = json_decode($content);
//get new access token and new refresh token...
$new_access_token = $response->access_token;
$new_access_token = $response->access_token;
$new_refresh_token = $response->refresh_token;
$this->assertTrue(!empty($new_access_token));
$this->assertTrue(!empty($new_refresh_token));
//do token validation ....
$params = array(
'token' => $new_access_token,
);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@introspection",
$params,
array(),
array(),
array(),
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret)));
$this->assertResponseStatus(200);
} catch (Exception $ex) {
throw $ex;
}