From f0a73f2b7a0d85b5fc117910f0c6c62ea62705f4 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 6 Dec 2019 18:31:04 +0300 Subject: [PATCH] Make tokens, created by client credentials grant to live forever --- api/components/OAuth2/Component.php | 6 +- api/components/Tokens/Component.php | 8 ++- api/components/Tokens/TokensFactory.php | 30 ++++++--- api/config/config.php | 16 ++--- .../oauth/ClientCredentialsCest.php | 3 +- .../unit/components/Tokens/ComponentTest.php | 6 +- .../components/Tokens/TokensFactoryTest.php | 66 ++++++++++++++++++- .../AuthenticationResultTest.php | 7 +- 8 files changed, 113 insertions(+), 29 deletions(-) diff --git a/api/components/OAuth2/Component.php b/api/components/OAuth2/Component.php index 125ea57..3712109 100644 --- a/api/components/OAuth2/Component.php +++ b/api/components/OAuth2/Component.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace api\components\OAuth2; use api\components\OAuth2\Keys\EmptyKey; +use Carbon\CarbonInterval; use DateInterval; use League\OAuth2\Server\AuthorizationServer; use yii\base\Component as BaseComponent; @@ -24,7 +25,7 @@ class Component extends BaseComponent { $authCodesRepo = new Repositories\AuthCodeRepository(); $refreshTokensRepo = new Repositories\RefreshTokenRepository(); - $accessTokenTTL = new DateInterval('P1D'); + $accessTokenTTL = CarbonInterval::day(); $authServer = new AuthorizationServer( $clientsRepo, @@ -44,9 +45,8 @@ class Component extends BaseComponent { $authServer->enableGrantType($refreshTokenGrant); $refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling - // TODO: make these access tokens live longer $clientCredentialsGrant = new Grants\ClientCredentialsGrant(); - $authServer->enableGrantType($clientCredentialsGrant, $accessTokenTTL); + $authServer->enableGrantType($clientCredentialsGrant, CarbonInterval::create(-1)); // set negative value to make it non expiring $clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling $this->_authServer = $authServer; diff --git a/api/components/Tokens/Component.php b/api/components/Tokens/Component.php index 5f0efbe..7294a74 100644 --- a/api/components/Tokens/Component.php +++ b/api/components/Tokens/Component.php @@ -55,9 +55,11 @@ class Component extends BaseComponent { public function create(array $payloads = [], array $headers = []): Token { $now = Carbon::now(); - $builder = (new Builder()) - ->issuedAt($now->getTimestamp()) - ->expiresAt($now->addHour()->getTimestamp()); + $builder = (new Builder())->issuedAt($now->getTimestamp()); + if (isset($payloads['exp'])) { + $builder->expiresAt($payloads['exp']); + } + foreach ($payloads as $claim => $value) { $builder->withClaim($claim, $this->prepareValue($value)); } diff --git a/api/components/Tokens/TokensFactory.php b/api/components/Tokens/TokensFactory.php index 4c29339..7adb800 100644 --- a/api/components/Tokens/TokensFactory.php +++ b/api/components/Tokens/TokensFactory.php @@ -8,6 +8,7 @@ use api\rbac\Roles as R; use Carbon\Carbon; use common\models\Account; use common\models\AccountSession; +use DateTime; use Lcobucci\JWT\Token; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; @@ -21,8 +22,9 @@ class TokensFactory extends Component { public function createForWebAccount(Account $account, AccountSession $session = null): Token { $payloads = [ - 'ely-scopes' => R::ACCOUNTS_WEB_USER, + 'ely-scopes' => $this->prepareScopes([R::ACCOUNTS_WEB_USER]), 'sub' => $this->buildSub($account->id), + 'exp' => Carbon::now()->addHour()->getTimestamp(), ]; if ($session === null) { // If we don't remember a session, the token should live longer @@ -38,11 +40,12 @@ class TokensFactory extends Component { public function createForOAuthClient(AccessTokenEntityInterface $accessToken): Token { $payloads = [ 'aud' => $this->buildAud($accessToken->getClient()->getIdentifier()), - 'ely-scopes' => $this->joinScopes(array_map(static function(ScopeEntityInterface $scope): string { - return $scope->getIdentifier(); - }, $accessToken->getScopes())), - 'exp' => $accessToken->getExpiryDateTime()->getTimestamp(), + 'ely-scopes' => $this->prepareScopes($accessToken->getScopes()), ]; + if ($accessToken->getExpiryDateTime() > new DateTime()) { + $payloads['exp'] = $accessToken->getExpiryDateTime()->getTimestamp(); + } + if ($accessToken->getUserIdentifier() !== null) { $payloads['sub'] = $this->buildSub($accessToken->getUserIdentifier()); } @@ -52,15 +55,26 @@ class TokensFactory extends Component { public function createForMinecraftAccount(Account $account, string $clientToken): Token { return Yii::$app->tokens->create([ - 'ely-scopes' => $this->joinScopes([P::MINECRAFT_SERVER_SESSION]), + 'ely-scopes' => $this->prepareScopes([P::MINECRAFT_SERVER_SESSION]), 'ely-client-token' => new EncryptedValue($clientToken), 'sub' => $this->buildSub($account->id), 'exp' => Carbon::now()->addDays(2)->getTimestamp(), ]); } - private function joinScopes(array $scopes): string { - return implode(',', $scopes); + /** + * @param ScopeEntityInterface[]|string[] $scopes + * + * @return string + */ + private function prepareScopes(array $scopes): string { + return implode(',', array_map(function($scope): string { + if ($scope instanceof ScopeEntityInterface) { + return $scope->getIdentifier(); + } + + return $scope; + }, $scopes)); } private function buildSub(int $accountId): string { diff --git a/api/config/config.php b/api/config/config.php index 115b8ab..0b3dda6 100644 --- a/api/config/config.php +++ b/api/config/config.php @@ -7,6 +7,14 @@ return [ 'params' => [ 'authserverHost' => getenv('AUTHSERVER_HOST') ?: 'authserver.ely.by', ], + 'modules' => [ + 'authserver' => api\modules\authserver\Module::class, + 'session' => api\modules\session\Module::class, + 'mojang' => api\modules\mojang\Module::class, + 'internal' => api\modules\internal\Module::class, + 'accounts' => api\modules\accounts\Module::class, + 'oauth' => api\modules\oauth\Module::class, + ], 'components' => [ 'user' => [ 'class' => api\components\User\Component::class, @@ -90,12 +98,4 @@ return [ 'class' => api\components\ErrorHandler::class, ], ], - 'modules' => [ - 'authserver' => api\modules\authserver\Module::class, - 'session' => api\modules\session\Module::class, - 'mojang' => api\modules\mojang\Module::class, - 'internal' => api\modules\internal\Module::class, - 'accounts' => api\modules\accounts\Module::class, - 'oauth' => api\modules\oauth\Module::class, - ], ]; diff --git a/api/tests/functional/oauth/ClientCredentialsCest.php b/api/tests/functional/oauth/ClientCredentialsCest.php index b83f78b..8712803 100644 --- a/api/tests/functional/oauth/ClientCredentialsCest.php +++ b/api/tests/functional/oauth/ClientCredentialsCest.php @@ -3,7 +3,6 @@ declare(strict_types=1); namespace api\tests\functional\oauth; -use api\tests\functional\_steps\OauthSteps; use api\tests\FunctionalTester; class ClientCredentialsCest { @@ -35,7 +34,7 @@ class ClientCredentialsCest { ]); } - public function issueTokenWithInternalScopesAsTrustedClient(OauthSteps $I) { + public function issueTokenWithInternalScopesAsTrustedClient(FunctionalTester $I) { $I->wantTo('issue token as trusted client and require some internal scope'); $I->sendPOST('/api/oauth2/v1/token', [ 'grant_type' => 'client_credentials', diff --git a/api/tests/unit/components/Tokens/ComponentTest.php b/api/tests/unit/components/Tokens/ComponentTest.php index d8a7018..e2d9e87 100644 --- a/api/tests/unit/components/Tokens/ComponentTest.php +++ b/api/tests/unit/components/Tokens/ComponentTest.php @@ -22,7 +22,11 @@ class ComponentTest extends TestCase { $this->assertSame('ES256', $token->getHeader('alg')); $this->assertEmpty(array_diff(array_keys($token->getClaims()), ['iat', 'exp'])); $this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 1); - $this->assertEqualsWithDelta(time() + 3600, $token->getClaim('exp'), 2); + + // Pass exp claim + $time = time() + 60; + $token = $this->component->create(['exp' => $time]); + $this->assertSame($time, $token->getClaim('exp')); // Pass custom payloads $token = $this->component->create(['find' => 'me']); diff --git a/api/tests/unit/components/Tokens/TokensFactoryTest.php b/api/tests/unit/components/Tokens/TokensFactoryTest.php index 7dc353b..36e536a 100644 --- a/api/tests/unit/components/Tokens/TokensFactoryTest.php +++ b/api/tests/unit/components/Tokens/TokensFactoryTest.php @@ -5,16 +5,22 @@ namespace api\tests\unit\components\Tokens; use api\components\Tokens\TokensFactory; use api\tests\unit\TestCase; +use Carbon\Carbon; use common\models\Account; use common\models\AccountSession; +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; class TokensFactoryTest extends TestCase { public function testCreateForAccount() { + $factory = new TokensFactory(); + $account = new Account(); $account->id = 1; - $factory = new TokensFactory(); + // Create for account $token = $factory->createForWebAccount($account); $this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 1); @@ -26,6 +32,8 @@ class TokensFactoryTest extends TestCase { $session = new AccountSession(); $session->id = 2; + // Create for account with remember me + $token = $factory->createForWebAccount($account, $session); $this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 1); $this->assertEqualsWithDelta(time() + 3600, $token->getClaim('exp'), 2); @@ -34,4 +42,60 @@ class TokensFactoryTest extends TestCase { $this->assertSame(2, $token->getClaim('jti')); } + public function testCreateForOauthClient() { + $factory = new TokensFactory(); + + $client = $this->createMock(ClientEntityInterface::class); + $client->method('getIdentifier')->willReturn('clientId'); + + $scope1 = $this->createMock(ScopeEntityInterface::class); + $scope1->method('getIdentifier')->willReturn('scope1'); + $scope2 = $this->createMock(ScopeEntityInterface::class); + $scope2->method('getIdentifier')->willReturn('scope2'); + + $expiryDateTime = Carbon::now()->addDay(); + + // Create for auth code grant + + $accessToken = $this->createMock(AccessTokenEntityInterface::class); + $accessToken->method('getClient')->willReturn($client); + $accessToken->method('getScopes')->willReturn([$scope1, $scope2]); + $accessToken->method('getExpiryDateTime')->willReturn($expiryDateTime); + $accessToken->method('getUserIdentifier')->willReturn(1); + + $token = $factory->createForOAuthClient($accessToken); + $this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 1); + $this->assertEqualsWithDelta($expiryDateTime->getTimestamp(), $token->getClaim('exp'), 2); + $this->assertSame('ely|1', $token->getClaim('sub')); + $this->assertSame('client|clientId', $token->getClaim('aud')); + $this->assertSame('scope1,scope2', $token->getClaim('ely-scopes')); + + // Create for client credentials grant + + $accessToken = $this->createMock(AccessTokenEntityInterface::class); + $accessToken->method('getClient')->willReturn($client); + $accessToken->method('getScopes')->willReturn([$scope1, $scope2]); + $accessToken->method('getExpiryDateTime')->willReturn(Carbon::now()->subDay()); + $accessToken->method('getUserIdentifier')->willReturn(null); + + $token = $factory->createForOAuthClient($accessToken); + $this->assertSame('no value', $token->getClaim('exp', 'no value')); + $this->assertSame('no value', $token->getClaim('sub', 'no value')); + } + + public function testCreateForMinecraftAccount() { + $factory = new TokensFactory(); + + $account = new Account(); + $account->id = 1; + $clientToken = 'e44fae79-f80e-4975-952e-47e8a9ed9472'; + + $token = $factory->createForMinecraftAccount($account, $clientToken); + $this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 5); + $this->assertEqualsWithDelta(time() + 60 * 60 * 24 * 2, $token->getClaim('exp'), 5); + $this->assertSame('minecraft_server_session', $token->getClaim('ely-scopes')); + $this->assertNotSame('e44fae79-f80e-4975-952e-47e8a9ed9472', $token->getClaim('ely-client-token')); + $this->assertSame('ely|1', $token->getClaim('sub')); + } + } diff --git a/api/tests/unit/models/authentication/AuthenticationResultTest.php b/api/tests/unit/models/authentication/AuthenticationResultTest.php index 0945b98..9439cc5 100644 --- a/api/tests/unit/models/authentication/AuthenticationResultTest.php +++ b/api/tests/unit/models/authentication/AuthenticationResultTest.php @@ -21,19 +21,20 @@ class AuthenticationResultTest extends TestCase { } public function testGetAsResponse() { - $token = Yii::$app->tokens->create(); + $time = time() + 3600; + $token = Yii::$app->tokens->create(['exp' => $time]); $jwt = (string)$token; $model = new AuthenticationResult($token); $result = $model->formatAsOAuth2Response(); $this->assertSame($jwt, $result['access_token']); - $this->assertEqualsWithDelta(3600, $result['expires_in'], 1); + $this->assertSame(3600, $result['expires_in']); $this->assertArrayNotHasKey('refresh_token', $result); $model = new AuthenticationResult($token, 'refresh_token'); $result = $model->formatAsOAuth2Response(); $this->assertSame($jwt, $result['access_token']); - $this->assertEqualsWithDelta(3600, $result['expires_in'], 1); + $this->assertSame(3600, $result['expires_in']); $this->assertSame('refresh_token', $result['refresh_token']); }