diff --git a/api/components/OAuth2/Component.php b/api/components/OAuth2/Component.php index fc65b5b..dd962a8 100644 --- a/api/components/OAuth2/Component.php +++ b/api/components/OAuth2/Component.php @@ -3,7 +3,6 @@ declare(strict_types=1); namespace api\components\OAuth2; -use api\components\OAuth2\Keys\EmptyKey; use Carbon\CarbonInterval; use DateInterval; use League\OAuth2\Server\AuthorizationServer; @@ -18,41 +17,45 @@ class Component extends BaseComponent { public function getAuthServer(): AuthorizationServer { if ($this->_authServer === null) { - $clientsRepo = new Repositories\ClientRepository(); - $accessTokensRepo = new Repositories\AccessTokenRepository(); - $publicScopesRepo = new Repositories\PublicScopeRepository(); - $internalScopesRepo = new Repositories\InternalScopeRepository(); - $authCodesRepo = new Repositories\AuthCodeRepository(); - $refreshTokensRepo = new Repositories\RefreshTokenRepository(); - - $accessTokenTTL = CarbonInterval::days(2); - - $authServer = new AuthorizationServer( - $clientsRepo, - $accessTokensRepo, - new Repositories\EmptyScopeRepository(), - new EmptyKey(), - '', // omit key because we use our own encryption mechanism - new ResponseTypes\BearerTokenResponse() - ); - /** @noinspection PhpUnhandledExceptionInspection */ - $authCodeGrant = new Grants\AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M')); - $authCodeGrant->disableRequireCodeChallengeForPublicClients(); - $authServer->enableGrantType($authCodeGrant, $accessTokenTTL); - $authCodeGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling - - $refreshTokenGrant = new Grants\RefreshTokenGrant($refreshTokensRepo); - $authServer->enableGrantType($refreshTokenGrant); - $refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling - - $clientCredentialsGrant = new Grants\ClientCredentialsGrant(); - $authServer->enableGrantType($clientCredentialsGrant, CarbonInterval::create(-1)); // set negative value to make it non expiring - $clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling - - $this->_authServer = $authServer; + $this->_authServer = $this->createAuthServer(); } return $this->_authServer; } + private function createAuthServer(): AuthorizationServer { + $clientsRepo = new Repositories\ClientRepository(); + $accessTokensRepo = new Repositories\AccessTokenRepository(); + $publicScopesRepo = new Repositories\PublicScopeRepository(); + $internalScopesRepo = new Repositories\InternalScopeRepository(); + $authCodesRepo = new Repositories\AuthCodeRepository(); + $refreshTokensRepo = new Repositories\RefreshTokenRepository(); + + $accessTokenTTL = CarbonInterval::create(-1); // Set negative value to make tokens non expiring + + $authServer = new AuthorizationServer( + $clientsRepo, + $accessTokensRepo, + new Repositories\EmptyScopeRepository(), + new Keys\EmptyKey(), + '', // Omit the key because we use our own encryption mechanism + new ResponseTypes\BearerTokenResponse() + ); + /** @noinspection PhpUnhandledExceptionInspection */ + $authCodeGrant = new Grants\AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M')); + $authCodeGrant->disableRequireCodeChallengeForPublicClients(); + $authServer->enableGrantType($authCodeGrant, $accessTokenTTL); + $authCodeGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling + + $refreshTokenGrant = new Grants\RefreshTokenGrant($refreshTokensRepo); + $authServer->enableGrantType($refreshTokenGrant, $accessTokenTTL); + $refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling + + $clientCredentialsGrant = new Grants\ClientCredentialsGrant(); + $authServer->enableGrantType($clientCredentialsGrant, $accessTokenTTL); + $clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling + + return $authServer; + } + } diff --git a/api/components/OAuth2/Entities/AccessTokenEntity.php b/api/components/OAuth2/Entities/AccessTokenEntity.php index a3c0f78..04c34e7 100644 --- a/api/components/OAuth2/Entities/AccessTokenEntity.php +++ b/api/components/OAuth2/Entities/AccessTokenEntity.php @@ -3,64 +3,22 @@ declare(strict_types=1); namespace api\components\OAuth2\Entities; -use api\components\OAuth2\Repositories\PublicScopeRepository; -use api\rbac\Permissions; -use Carbon\CarbonImmutable; -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; use Yii; class AccessTokenEntity implements AccessTokenEntityInterface { use EntityTrait; - use TokenEntityTrait { - getExpiryDateTime as parentGetExpiryDateTime; - } + use TokenEntityTrait; - /** - * 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 { - $scopes = $this->scopes; - $this->scopes = array_filter($this->scopes, function(ScopeEntityInterface $scope): bool { - return $scope->getIdentifier() !== PublicScopeRepository::OFFLINE_ACCESS; - }); - - $token = Yii::$app->tokensFactory->createForOAuthClient($this); - - $this->scopes = $scopes; - - return (string)$token; + return (string)Yii::$app->tokensFactory->createForOAuthClient($this); } 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(): DateTimeImmutable { - $expiryTime = $this->parentGetExpiryDateTime(); - if ($this->hasScope(PublicScopeRepository::CHANGE_SKIN) || $this->hasScope(Permissions::OBTAIN_ACCOUNT_EMAIL)) { - $expiryTime = min($expiryTime, CarbonImmutable::now()->addHour()); - } - - return $expiryTime; - } - - private function hasScope(string $scopeIdentifier): bool { - foreach ($this->getScopes() as $scope) { - if ($scope->getIdentifier() === $scopeIdentifier) { - return true; - } - } - - return false; - } - } diff --git a/api/components/OAuth2/Entities/RefreshTokenEntity.php b/api/components/OAuth2/Entities/RefreshTokenEntity.php deleted file mode 100644 index 0b8383d..0000000 --- a/api/components/OAuth2/Entities/RefreshTokenEntity.php +++ /dev/null @@ -1,29 +0,0 @@ -getScopes() as $scope) { + /** + * @param DateInterval $accessTokenTTL + * @param ClientEntityInterface $client + * @param string|null $userIdentifier + * @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes + * + * @return AccessTokenEntityInterface + * @throws \League\OAuth2\Server\Exception\OAuthServerException + * @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException + */ + protected function issueAccessToken( + DateInterval $accessTokenTTL, + ClientEntityInterface $client, + $userIdentifier, + array $scopes = [] + ): AccessTokenEntityInterface { + foreach ($scopes as $i => $scope) { if ($scope->getIdentifier() === PublicScopeRepository::OFFLINE_ACCESS) { - return parent::issueRefreshToken($accessToken); + unset($scopes[$i]); + $this->getEmitter()->emit(new RequestedRefreshToken()); } } - return null; + return parent::issueAccessToken($accessTokenTTL, $client, $userIdentifier, $scopes); } } diff --git a/api/components/OAuth2/Grants/RefreshTokenGrant.php b/api/components/OAuth2/Grants/RefreshTokenGrant.php index 18f070e..675d388 100644 --- a/api/components/OAuth2/Grants/RefreshTokenGrant.php +++ b/api/components/OAuth2/Grants/RefreshTokenGrant.php @@ -4,7 +4,11 @@ declare(strict_types=1); namespace api\components\OAuth2\Grants; use api\components\OAuth2\CryptTrait; +use api\components\Tokens\TokenReader; +use Carbon\Carbon; use common\models\OauthSession; +use InvalidArgumentException; +use Lcobucci\JWT\ValidationData; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; @@ -32,7 +36,7 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant { return $this->validateLegacyRefreshToken($refreshToken); } - return parent::validateOldRefreshToken($request, $clientId); + return $this->validateAccessToken($refreshToken); } /** @@ -84,4 +88,36 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant { ]; } + /** + * @param string $jwt + * @return array + * @throws OAuthServerException + */ + private function validateAccessToken(string $jwt): array { + try { + $token = Yii::$app->tokens->parse($jwt); + } catch (InvalidArgumentException $e) { + throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e); + } + + if (!Yii::$app->tokens->verify($token)) { + throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token'); + } + + if (!$token->validate(new ValidationData(Carbon::now()->getTimestamp()))) { + throw OAuthServerException::invalidRefreshToken('Token has expired'); + } + + $reader = new TokenReader($token); + + return [ + 'client_id' => $reader->getClientId(), + 'refresh_token_id' => '', // This value used only to invalidate old token + 'access_token_id' => '', // This value used only to invalidate old token + 'scopes' => $reader->getScopes(), + 'user_id' => $reader->getAccountId(), + 'expire_time' => null, + ]; + } + } diff --git a/api/components/OAuth2/Repositories/RefreshTokenRepository.php b/api/components/OAuth2/Repositories/RefreshTokenRepository.php index b1096ed..199e342 100644 --- a/api/components/OAuth2/Repositories/RefreshTokenRepository.php +++ b/api/components/OAuth2/Repositories/RefreshTokenRepository.php @@ -3,34 +3,25 @@ declare(strict_types=1); namespace api\components\OAuth2\Repositories; -use api\components\OAuth2\Entities\RefreshTokenEntity; -use common\models\OauthRefreshToken; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; -use Webmozart\Assert\Assert; class RefreshTokenRepository implements RefreshTokenRepositoryInterface { public function getNewRefreshToken(): ?RefreshTokenEntityInterface { - return new RefreshTokenEntity(); + return null; } public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void { - $model = new OauthRefreshToken(); - $model->id = $refreshTokenEntity->getIdentifier(); - $model->account_id = $refreshTokenEntity->getAccessToken()->getUserIdentifier(); - $model->client_id = $refreshTokenEntity->getAccessToken()->getClient()->getIdentifier(); - - Assert::true($model->save()); + // Do nothing } public function revokeRefreshToken($tokenId): void { - // Currently we're not rotating refresh tokens so do not revoke - // token during any OAuth2 grant + // Do nothing } public function isRefreshTokenRevoked($tokenId): bool { - return OauthRefreshToken::find()->andWhere(['id' => $tokenId])->exists() === false; + return false; } } diff --git a/api/components/Tokens/TokenReader.php b/api/components/Tokens/TokenReader.php new file mode 100644 index 0000000..95fb934 --- /dev/null +++ b/api/components/Tokens/TokenReader.php @@ -0,0 +1,64 @@ +token = $token; + } + + public function getAccountId(): ?int { + $sub = $this->token->getClaim('sub', false); + if ($sub === false) { + return null; + } + + if (mb_strpos((string)$sub, TokensFactory::SUB_ACCOUNT_PREFIX) !== 0) { + return null; + } + + return (int)mb_substr($sub, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX)); + } + + public function getClientId(): ?string { + $aud = $this->token->getClaim('aud', false); + if ($aud === false) { + return null; + } + + if (mb_strpos((string)$aud, TokensFactory::AUD_CLIENT_PREFIX) !== 0) { + return null; + } + + return mb_substr($aud, mb_strlen(TokensFactory::AUD_CLIENT_PREFIX)); + } + + public function getScopes(): ?array { + $scopes = $this->token->getClaim('ely-scopes', false); + if ($scopes === false) { + return null; + } + + return explode(',', $scopes); + } + + public function getMinecraftClientToken(): ?string { + $encodedClientToken = $this->token->getClaim('ely-client-token', false); + if ($encodedClientToken === false) { + return null; + } + + return Yii::$app->tokens->decryptValue($encodedClientToken); + } + +} diff --git a/api/components/Tokens/TokensFactory.php b/api/components/Tokens/TokensFactory.php index 7adb800..0b7837c 100644 --- a/api/components/Tokens/TokensFactory.php +++ b/api/components/Tokens/TokensFactory.php @@ -68,7 +68,7 @@ class TokensFactory extends Component { * @return string */ private function prepareScopes(array $scopes): string { - return implode(',', array_map(function($scope): string { + return implode(',', array_map(function($scope): string { // TODO: replace to the space if it's possible if ($scope instanceof ScopeEntityInterface) { return $scope->getIdentifier(); } diff --git a/api/components/User/JwtIdentity.php b/api/components/User/JwtIdentity.php index e327e32..34ee296 100644 --- a/api/components/User/JwtIdentity.php +++ b/api/components/User/JwtIdentity.php @@ -3,13 +3,12 @@ declare(strict_types=1); namespace api\components\User; -use api\components\Tokens\TokensFactory; +use api\components\Tokens\TokenReader; use Carbon\Carbon; use common\models\Account; use Exception; use Lcobucci\JWT\Token; use Lcobucci\JWT\ValidationData; -use Webmozart\Assert\Assert; use Yii; use yii\base\NotSupportedException; use yii\web\UnauthorizedHttpException; @@ -21,6 +20,11 @@ class JwtIdentity implements IdentityInterface { */ private $token; + /** + * @var TokenReader|null + */ + private $reader; + private function __construct(Token $token) { $this->token = $token; } @@ -46,11 +50,6 @@ class JwtIdentity implements IdentityInterface { throw new UnauthorizedHttpException('Incorrect token'); } - $sub = $token->getClaim('sub', false); - if ($sub !== false && strpos((string)$sub, TokensFactory::SUB_ACCOUNT_PREFIX) !== 0) { - throw new UnauthorizedHttpException('Incorrect token'); - } - return new self($token); } @@ -59,24 +58,11 @@ class JwtIdentity implements IdentityInterface { } public function getAccount(): ?Account { - $subject = $this->token->getClaim('sub', false); - if ($subject === false) { - return null; - } - - Assert::startsWith($subject, TokensFactory::SUB_ACCOUNT_PREFIX); - $accountId = (int)mb_substr($subject, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX)); - - return Account::findOne(['id' => $accountId]); + return Account::findOne(['id' => $this->getReader()->getAccountId()]); } public function getAssignedPermissions(): array { - $scopesClaim = $this->token->getClaim('ely-scopes', false); - if ($scopesClaim === false) { - return []; - } - - return explode(',', $scopesClaim); + return $this->getReader()->getScopes() ?? []; } public function getId(): string { @@ -98,4 +84,12 @@ class JwtIdentity implements IdentityInterface { // @codeCoverageIgnoreEnd + private function getReader(): TokenReader { + if ($this->reader === null) { + $this->reader = new TokenReader($this->token); + } + + return $this->reader; + } + } diff --git a/api/modules/authserver/models/RefreshTokenForm.php b/api/modules/authserver/models/RefreshTokenForm.php index d719c8d..f9adff1 100644 --- a/api/modules/authserver/models/RefreshTokenForm.php +++ b/api/modules/authserver/models/RefreshTokenForm.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace api\modules\authserver\models; +use api\components\Tokens\TokenReader; use api\models\base\ApiForm; use api\modules\authserver\exceptions\ForbiddenOperationException; use api\modules\authserver\validators\AccessTokenValidator; @@ -49,16 +50,12 @@ class RefreshTokenForm extends ApiForm { } } else { $token = Yii::$app->tokens->parse($this->accessToken); - - $encodedClientToken = $token->getClaim('ely-client-token'); - $clientToken = Yii::$app->tokens->decryptValue($encodedClientToken); - if ($clientToken !== $this->clientToken) { + $tokenReader = new TokenReader($token); + if ($tokenReader->getMinecraftClientToken() !== $this->clientToken) { throw new ForbiddenOperationException('Invalid token.'); } - $accountClaim = $token->getClaim('sub'); - $accountId = (int)explode('|', $accountClaim)[1]; - $account = Account::findOne(['id' => $accountId]); + $account = Account::findOne(['id' => $tokenReader->getAccountId()]); } if ($account === null) { diff --git a/api/modules/oauth/controllers/AuthorizationController.php b/api/modules/oauth/controllers/AuthorizationController.php index 85c1616..d54d21d 100644 --- a/api/modules/oauth/controllers/AuthorizationController.php +++ b/api/modules/oauth/controllers/AuthorizationController.php @@ -6,6 +6,8 @@ namespace api\modules\oauth\controllers; use api\controllers\Controller; use api\modules\oauth\models\OauthProcess; use api\rbac\Permissions as P; +use GuzzleHttp\Psr7\ServerRequest; +use Psr\Http\Message\ServerRequestInterface; use Yii; use yii\filters\AccessControl; use yii\helpers\ArrayHelper; @@ -45,19 +47,23 @@ class AuthorizationController extends Controller { } public function actionValidate(): array { - return $this->createOauthProcess()->validate(); + return $this->createOauthProcess()->validate($this->getServerRequest()); } public function actionComplete(): array { - return $this->createOauthProcess()->complete(); + return $this->createOauthProcess()->complete($this->getServerRequest()); } public function actionToken(): array { - return $this->createOauthProcess()->getToken(); + return $this->createOauthProcess()->getToken($this->getServerRequest()); } private function createOauthProcess(): OauthProcess { return new OauthProcess(Yii::$app->oauth->getAuthServer()); } + private function getServerRequest(): ServerRequestInterface { + return ServerRequest::fromGlobals(); + } + } diff --git a/api/modules/oauth/models/OauthProcess.php b/api/modules/oauth/models/OauthProcess.php index fca30dd..761c49d 100644 --- a/api/modules/oauth/models/OauthProcess.php +++ b/api/modules/oauth/models/OauthProcess.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace api\modules\oauth\models; use api\components\OAuth2\Entities\UserEntity; +use api\components\OAuth2\Events\RequestedRefreshToken; use api\rbac\Permissions as P; use common\models\Account; use common\models\OauthClient; use common\models\OauthSession; use GuzzleHttp\Psr7\Response; -use GuzzleHttp\Psr7\ServerRequest; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; @@ -20,6 +20,7 @@ use Yii; class OauthProcess { + // TODO: merge this with PublicScopesRepository private const INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [ P::OBTAIN_OWN_ACCOUNT_INFO => 'account_info', P::OBTAIN_ACCOUNT_EMAIL => 'account_email', @@ -49,11 +50,11 @@ class OauthProcess { * * In addition, you can pass the description value to override the application's description. * + * @param ServerRequestInterface $request * @return array */ - public function validate(): array { + public function validate(ServerRequestInterface $request): array { try { - $request = $this->getRequest(); $authRequest = $this->server->validateAuthorizationRequest($request); $client = $authRequest->getClient(); /** @var OauthClient $clientModel */ @@ -83,13 +84,13 @@ class OauthProcess { * If the field is present, it will be interpreted as any value resulting in false positives. * Otherwise, the value will be interpreted as "true". * + * @param ServerRequestInterface $request * @return array */ - public function complete(): array { + public function complete(ServerRequestInterface $request): array { try { Yii::$app->statsd->inc('oauth.complete.attempt'); - $request = $this->getRequest(); $authRequest = $this->server->validateAuthorizationRequest($request); /** @var Account $account */ $account = Yii::$app->user->identity->getAccount(); @@ -151,18 +152,29 @@ class OauthProcess { * grant_type, * ] * + * @param ServerRequestInterface $request * @return array */ - public function getToken(): array { - $request = $this->getRequest(); + public function getToken(ServerRequestInterface $request): array { $params = (array)$request->getParsedBody(); $clientId = $params['client_id'] ?? ''; $grantType = $params['grant_type'] ?? 'null'; try { Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.attempt"); + $shouldIssueRefreshToken = false; + $this->server->getEmitter()->addOneTimeListener(RequestedRefreshToken::class, function() use (&$shouldIssueRefreshToken) { + $shouldIssueRefreshToken = true; + }); + $response = $this->server->respondToAccessTokenRequest($request, new Response(200)); + /** @noinspection JsonEncodingApiUsageInspection at this point json error is not possible */ $result = json_decode((string)$response->getBody(), true); + if ($shouldIssueRefreshToken) { + // Set the refresh_token field to keep compatibility with the old clients, + // which will be broken in case when refresh_token field will be missing + $result['refresh_token'] = $result['access_token']; + } Yii::$app->statsd->inc("oauth.issueToken_client.{$clientId}"); Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.success"); @@ -312,10 +324,6 @@ class OauthProcess { ]; } - private function getRequest(): ServerRequestInterface { - return ServerRequest::fromGlobals(); - } - private function createAcceptRequiredException(): OAuthServerException { return new OAuthServerException( 'Client must accept authentication request.', diff --git a/api/tests/functional/oauth/RefreshTokenCest.php b/api/tests/functional/oauth/RefreshTokenCest.php index edc68bb..7b5997c 100644 --- a/api/tests/functional/oauth/RefreshTokenCest.php +++ b/api/tests/functional/oauth/RefreshTokenCest.php @@ -27,7 +27,7 @@ class RefreshTokenCest { 'refresh_token' => $refreshToken, 'client_id' => 'ely', 'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM', - 'scope' => 'minecraft_server_session offline_access', + 'scope' => 'minecraft_server_session', ]); $this->canSeeRefreshTokenSuccess($I); } diff --git a/api/tests/unit/components/OAuth2/Entities/AccessTokenEntityTest.php b/api/tests/unit/components/OAuth2/Entities/AccessTokenEntityTest.php index a43a664..83a8af7 100644 --- a/api/tests/unit/components/OAuth2/Entities/AccessTokenEntityTest.php +++ b/api/tests/unit/components/OAuth2/Entities/AccessTokenEntityTest.php @@ -5,7 +5,6 @@ namespace api\tests\unit\components\OAuth2\Entities; use api\components\OAuth2\Entities\AccessTokenEntity; use api\tests\unit\TestCase; -use DateInterval; use DateTimeImmutable; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; @@ -22,35 +21,10 @@ class AccessTokenEntityTest extends TestCase { $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()); - } - - public function testGetExpiryDateTime() { - $initialExpiry = (new DateTimeImmutable())->add(new DateInterval('P1D')); - - $entity = new AccessTokenEntity(); - $entity->setExpiryDateTime($initialExpiry); - $this->assertSame($initialExpiry, $entity->getExpiryDateTime()); - - $entity = new AccessTokenEntity(); - $entity->setExpiryDateTime($initialExpiry); - $entity->addScope($this->createScopeEntity('change_skin')); - $this->assertEqualsWithDelta(time() + 60 * 60, $entity->getExpiryDateTime()->getTimestamp(), 5); - - $entity = new AccessTokenEntity(); - $entity->setExpiryDateTime($initialExpiry); - $entity->addScope($this->createScopeEntity('obtain_account_email')); - $this->assertEqualsWithDelta(time() + 60 * 60, $entity->getExpiryDateTime()->getTimestamp(), 5); + $this->assertSame('first,second', $payloads['ely-scopes']); } private function createScopeEntity(string $id): ScopeEntityInterface { diff --git a/api/tests/unit/components/User/JwtIdentityTest.php b/api/tests/unit/components/User/JwtIdentityTest.php index ef8c082..cc91a25 100644 --- a/api/tests/unit/components/User/JwtIdentityTest.php +++ b/api/tests/unit/components/User/JwtIdentityTest.php @@ -50,10 +50,6 @@ class JwtIdentityTest extends TestCase { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.yth31f2PyhUkYSfBlizzUXWIgOvxxk8gNP-js0z8g1OT5rig40FPTIkgsZRctAwAAlj6QoIWW7-hxLTcSb2vmw', 'Incorrect token', ]; - yield 'invalid sub' => [ - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoxMjM0fQ.yigP5nWFdX0ktbuZC_Unb9bWxpAVd7Nv8Fb1Vsa0t5WkVA88VbhPi2P-CenbDOy8ngwoGV9m3c3upMs2V3gqvw', - 'Incorrect token', - ]; yield 'empty token' => ['', 'Incorrect token']; } @@ -66,6 +62,10 @@ class JwtIdentityTest extends TestCase { $identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDk5OTk5In0.1pAnhkR-_ZqzjLBR-PNIMJUXRSUK3aYixrFNKZg2ynPNPiDvzh8U-iBTT6XRfMP5nvfXZucRpoPVoiXtx40CUQ'); $this->assertNull($identity->getAccount()); + // Sub contains invalid value + $identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoxMjM0fQ.yigP5nWFdX0ktbuZC_Unb9bWxpAVd7Nv8Fb1Vsa0t5WkVA88VbhPi2P-CenbDOy8ngwoGV9m3c3upMs2V3gqvw'); + $this->assertNull($identity->getAccount()); + // Token without sub claim $identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Mn0.QxmYgSflZOQmhzYRr8bowU767yu4yKgTVaho0MPuyCmUfZO_0O0SQASMKVILf-wlT0ODTTG7vD753a2MTAmPmw'); $this->assertNull($identity->getAccount()); diff --git a/common/models/Account.php b/common/models/Account.php index af104a3..fc227b8 100644 --- a/common/models/Account.php +++ b/common/models/Account.php @@ -41,7 +41,6 @@ use const common\LATEST_RULES_VERSION; * @property UsernameHistory[] $usernameHistory * @property AccountSession[] $sessions * @property MinecraftAccessKey[] $minecraftAccessKeys - * @property-read OauthRefreshToken[] $oauthRefreshTokens * * Behaviors: * @mixin TimestampBehavior @@ -102,10 +101,6 @@ class Account extends ActiveRecord { return $this->hasMany(OauthClient::class, ['account_id' => 'id']); } - public function getOauthRefreshTokens(): ActiveQuery { - return $this->hasMany(OauthRefreshToken::class, ['account_id' => 'id']); - } - public function getUsernameHistory(): ActiveQuery { return $this->hasMany(UsernameHistory::class, ['account_id' => 'id']); } diff --git a/common/models/OauthClient.php b/common/models/OauthClient.php index 259bf05..cad84a0 100644 --- a/common/models/OauthClient.php +++ b/common/models/OauthClient.php @@ -26,7 +26,6 @@ use yii\db\ActiveRecord; * Behaviors: * @property Account|null $account * @property OauthSession[] $sessions - * @property-read OauthRefreshToken[] $refreshTokens */ class OauthClient extends ActiveRecord { @@ -58,10 +57,6 @@ class OauthClient extends ActiveRecord { return $this->hasMany(OauthSession::class, ['client_id' => 'id']); } - public function getRefreshTokens(): ActiveQuery { - return $this->hasMany(OauthRefreshToken::class, ['client_id' => 'id']); - } - public static function find(): OauthClientQuery { return Yii::createObject(OauthClientQuery::class, [static::class]); } diff --git a/common/models/OauthRefreshToken.php b/common/models/OauthRefreshToken.php deleted file mode 100644 index e65aa00..0000000 --- a/common/models/OauthRefreshToken.php +++ /dev/null @@ -1,50 +0,0 @@ - TimestampBehavior::class, - 'createdAtAttribute' => 'issued_at', - 'updatedAtAttribute' => false, - ], - ]; - } - - public function getSession(): ActiveQuery { - return $this->hasOne(OauthSession::class, ['account_id' => 'account_id', 'client_id' => 'client_id']); - } - - public function getAccount(): ActiveQuery { - return $this->hasOne(Account::class, ['id' => 'account_id']); - } - - public function getClient(): ActiveQuery { - return $this->hasOne(OauthClient::class, ['id' => 'client_id']); - } - -} diff --git a/common/models/OauthSession.php b/common/models/OauthSession.php index fb0e4d6..aaa5c7e 100644 --- a/common/models/OauthSession.php +++ b/common/models/OauthSession.php @@ -19,7 +19,6 @@ use yii\db\ActiveRecord; * Relations: * @property-read OauthClient $client * @property-read Account $account - * @property-read OauthRefreshToken[] $refreshTokens */ class OauthSession extends ActiveRecord { @@ -44,10 +43,6 @@ class OauthSession extends ActiveRecord { return $this->hasOne(Account::class, ['id' => 'owner_id']); } - public function getRefreshTokens(): ActiveQuery { - return $this->hasMany(OauthRefreshToken::class, ['account_id' => 'account_id', 'client_id' => 'client_id']); - } - public function getScopes(): array { if (empty($this->scopes) && $this->legacy_id !== null) { return Yii::$app->redis->smembers($this->getLegacyRedisScopesKey()); diff --git a/common/tests/_support/FixtureHelper.php b/common/tests/_support/FixtureHelper.php index 31b4070..4c7f6a3 100644 --- a/common/tests/_support/FixtureHelper.php +++ b/common/tests/_support/FixtureHelper.php @@ -55,7 +55,6 @@ class FixtureHelper extends Module { 'legacyOauthSessionsScopes' => fixtures\LegacyOauthSessionScopeFixtures::class, 'legacyOauthAccessTokens' => fixtures\LegacyOauthAccessTokenFixture::class, 'legacyOauthAccessTokensScopes' => fixtures\LegacyOauthAccessTokenScopeFixture::class, - 'oauthRefreshTokens' => fixtures\OauthRefreshTokensFixture::class, 'legacyOauthRefreshTokens' => fixtures\LegacyOauthRefreshTokenFixture::class, 'minecraftAccessKeys' => fixtures\MinecraftAccessKeyFixture::class, ]; diff --git a/common/tests/fixtures/OauthRefreshTokensFixture.php b/common/tests/fixtures/OauthRefreshTokensFixture.php deleted file mode 100644 index 85345d2..0000000 --- a/common/tests/fixtures/OauthRefreshTokensFixture.php +++ /dev/null @@ -1,19 +0,0 @@ -addForeignKey('FK_oauth_session_to_account', 'oauth_sessions', 'account_id', 'accounts', 'id', 'CASCADE', 'CASCADE'); $this->addForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions', 'client_id', 'oauth_clients', 'id', 'CASCADE', 'CASCADE'); $this->addColumn('oauth_sessions', 'scopes', $this->json()->toString('scopes') . ' AFTER `legacy_id`'); - - $this->createTable('oauth_refresh_tokens', [ - 'id' => $this->string(80)->notNull()->unique(), - 'account_id' => $this->db->getTableSchema('oauth_sessions', true)->getColumn('account_id')->dbType . ' NOT NULL', - 'client_id' => $this->db->getTableSchema('oauth_sessions', true)->getColumn('client_id')->dbType . ' NOT NULL', - 'issued_at' => $this->integer(11)->unsigned()->notNull(), - $this->primary('id'), - ]); - $this->addForeignKey('FK_oauth_refresh_token_to_oauth_session', 'oauth_refresh_tokens', ['account_id', 'client_id'], 'oauth_sessions', ['account_id', 'client_id'], 'CASCADE'); } public function safeDown() { - $this->dropTable('oauth_refresh_tokens'); - $this->dropColumn('oauth_sessions', 'scopes'); $this->dropForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions'); $this->dropForeignKey('FK_oauth_session_to_account', 'oauth_sessions');