diff --git a/api/components/OAuth2/Entities/AccessTokenEntity.php b/api/components/OAuth2/Entities/AccessTokenEntity.php index f9441fe..7b077ef 100644 --- a/api/components/OAuth2/Entities/AccessTokenEntity.php +++ b/api/components/OAuth2/Entities/AccessTokenEntity.php @@ -3,9 +3,12 @@ declare(strict_types=1); namespace api\components\OAuth2\Entities; +use api\components\OAuth2\Repositories\PublicScopeRepository; use api\components\Tokens\TokensFactory; +use DateTimeImmutable; use League\OAuth2\Server\CryptKeyInterface; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\Traits\EntityTrait; use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; @@ -15,16 +18,31 @@ class AccessTokenEntity implements AccessTokenEntityInterface { getExpiryDateTime as parentGetExpiryDateTime; } + /** + * There is no need to store offline_access scope in the resulting access_token. + * We cannot remove it from the token because otherwise we won't be able to form a refresh_token. + * That's why we delete offline_access before creating the token and then return it back. + * + * @return string + */ public function __toString(): string { - // TODO: strip "offline_access" scope from the scopes list - return (string)TokensFactory::createForOAuthClient($this); + $scopes = $this->scopes; + $this->scopes = array_filter($this->scopes, function(ScopeEntityInterface $scope): bool { + return $scope->getIdentifier() !== PublicScopeRepository::OFFLINE_ACCESS; + }); + + $token = TokensFactory::createForOAuthClient($this); + + $this->scopes = $scopes; + + return (string)$token; } public function setPrivateKey(CryptKeyInterface $privateKey): void { // We use a general-purpose component to build JWT tokens, so there is no need to keep the key } - public function getExpiryDateTime() { + public function getExpiryDateTime(): DateTimeImmutable { // TODO: extend token life depending on scopes list return $this->parentGetExpiryDateTime(); } diff --git a/api/tests/unit/components/OAuth2/Entities/AccessTokenEntityTest.php b/api/tests/unit/components/OAuth2/Entities/AccessTokenEntityTest.php new file mode 100644 index 0000000..959c52e --- /dev/null +++ b/api/tests/unit/components/OAuth2/Entities/AccessTokenEntityTest.php @@ -0,0 +1,44 @@ +createMock(ClientEntityInterface::class); + $client->method('getIdentifier')->willReturn('mockClientId'); + + $entity = new AccessTokenEntity(); + $entity->setClient($client); + $entity->setExpiryDateTime(new \DateTimeImmutable()); + $entity->addScope($this->createScopeEntity('first')); + $entity->addScope($this->createScopeEntity('second')); + $entity->addScope($this->createScopeEntity('offline_access')); + + $token = (string)$entity; + $payloads = json_decode(base64_decode(explode('.', $token)[1]), true); + $this->assertStringNotContainsString('offline_access', $payloads['ely-scopes']); + + $scopes = $entity->getScopes(); + $this->assertCount(3, $scopes); + $this->assertSame('first', $scopes[0]->getIdentifier()); + $this->assertSame('second', $scopes[1]->getIdentifier()); + $this->assertSame('offline_access', $scopes[2]->getIdentifier()); + } + + private function createScopeEntity(string $id): ScopeEntityInterface { + /** @var ScopeEntityInterface|\PHPUnit\Framework\MockObject\MockObject $entity */ + $entity = $this->createMock(ScopeEntityInterface::class); + $entity->method('getIdentifier')->willReturn($id); + + return $entity; + } + +}