mirror of
https://github.com/elyby/accounts.git
synced 2024-11-06 08:11:24 +05:30
Remove refresh_token from OAuth2 result. Return the same access_token as a refresh_token in case when it's requested. Make access_tokens to live forever.
This commit is contained in:
parent
efb97a2006
commit
ba7fad84a0
@ -3,7 +3,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace api\components\OAuth2;
|
namespace api\components\OAuth2;
|
||||||
|
|
||||||
use api\components\OAuth2\Keys\EmptyKey;
|
|
||||||
use Carbon\CarbonInterval;
|
use Carbon\CarbonInterval;
|
||||||
use DateInterval;
|
use DateInterval;
|
||||||
use League\OAuth2\Server\AuthorizationServer;
|
use League\OAuth2\Server\AuthorizationServer;
|
||||||
@ -18,6 +17,13 @@ class Component extends BaseComponent {
|
|||||||
|
|
||||||
public function getAuthServer(): AuthorizationServer {
|
public function getAuthServer(): AuthorizationServer {
|
||||||
if ($this->_authServer === null) {
|
if ($this->_authServer === null) {
|
||||||
|
$this->_authServer = $this->createAuthServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_authServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAuthServer(): AuthorizationServer {
|
||||||
$clientsRepo = new Repositories\ClientRepository();
|
$clientsRepo = new Repositories\ClientRepository();
|
||||||
$accessTokensRepo = new Repositories\AccessTokenRepository();
|
$accessTokensRepo = new Repositories\AccessTokenRepository();
|
||||||
$publicScopesRepo = new Repositories\PublicScopeRepository();
|
$publicScopesRepo = new Repositories\PublicScopeRepository();
|
||||||
@ -25,14 +31,14 @@ class Component extends BaseComponent {
|
|||||||
$authCodesRepo = new Repositories\AuthCodeRepository();
|
$authCodesRepo = new Repositories\AuthCodeRepository();
|
||||||
$refreshTokensRepo = new Repositories\RefreshTokenRepository();
|
$refreshTokensRepo = new Repositories\RefreshTokenRepository();
|
||||||
|
|
||||||
$accessTokenTTL = CarbonInterval::days(2);
|
$accessTokenTTL = CarbonInterval::create(-1); // Set negative value to make tokens non expiring
|
||||||
|
|
||||||
$authServer = new AuthorizationServer(
|
$authServer = new AuthorizationServer(
|
||||||
$clientsRepo,
|
$clientsRepo,
|
||||||
$accessTokensRepo,
|
$accessTokensRepo,
|
||||||
new Repositories\EmptyScopeRepository(),
|
new Repositories\EmptyScopeRepository(),
|
||||||
new EmptyKey(),
|
new Keys\EmptyKey(),
|
||||||
'', // omit key because we use our own encryption mechanism
|
'', // Omit the key because we use our own encryption mechanism
|
||||||
new ResponseTypes\BearerTokenResponse()
|
new ResponseTypes\BearerTokenResponse()
|
||||||
);
|
);
|
||||||
/** @noinspection PhpUnhandledExceptionInspection */
|
/** @noinspection PhpUnhandledExceptionInspection */
|
||||||
@ -42,17 +48,14 @@ class Component extends BaseComponent {
|
|||||||
$authCodeGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
$authCodeGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||||
|
|
||||||
$refreshTokenGrant = new Grants\RefreshTokenGrant($refreshTokensRepo);
|
$refreshTokenGrant = new Grants\RefreshTokenGrant($refreshTokensRepo);
|
||||||
$authServer->enableGrantType($refreshTokenGrant);
|
$authServer->enableGrantType($refreshTokenGrant, $accessTokenTTL);
|
||||||
$refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
$refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||||
|
|
||||||
$clientCredentialsGrant = new Grants\ClientCredentialsGrant();
|
$clientCredentialsGrant = new Grants\ClientCredentialsGrant();
|
||||||
$authServer->enableGrantType($clientCredentialsGrant, CarbonInterval::create(-1)); // set negative value to make it non expiring
|
$authServer->enableGrantType($clientCredentialsGrant, $accessTokenTTL);
|
||||||
$clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling
|
$clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling
|
||||||
|
|
||||||
$this->_authServer = $authServer;
|
return $authServer;
|
||||||
}
|
|
||||||
|
|
||||||
return $this->_authServer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,64 +3,22 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace api\components\OAuth2\Entities;
|
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\CryptKeyInterface;
|
||||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
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\EntityTrait;
|
||||||
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
|
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
|
||||||
use Yii;
|
use Yii;
|
||||||
|
|
||||||
class AccessTokenEntity implements AccessTokenEntityInterface {
|
class AccessTokenEntity implements AccessTokenEntityInterface {
|
||||||
use EntityTrait;
|
use EntityTrait;
|
||||||
use TokenEntityTrait {
|
use TokenEntityTrait;
|
||||||
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 {
|
public function __toString(): string {
|
||||||
$scopes = $this->scopes;
|
return (string)Yii::$app->tokensFactory->createForOAuthClient($this);
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setPrivateKey(CryptKeyInterface $privateKey): void {
|
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
|
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace api\components\OAuth2\Entities;
|
|
||||||
|
|
||||||
use Carbon\CarbonImmutable;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
|
||||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
|
||||||
use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait;
|
|
||||||
|
|
||||||
class RefreshTokenEntity implements RefreshTokenEntityInterface {
|
|
||||||
use EntityTrait;
|
|
||||||
use RefreshTokenTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We don't rotate refresh tokens, so that to always pass validation in the internal validator
|
|
||||||
* of the oauth2 server implementation we set the lifetime as far as possible.
|
|
||||||
*
|
|
||||||
* In 2038 this may cause problems, but I am sure that by then this code, if it still works,
|
|
||||||
* will be rewritten several times and the problem will be solved in a completely different way.
|
|
||||||
*
|
|
||||||
* @return DateTimeImmutable
|
|
||||||
*/
|
|
||||||
public function getExpiryDateTime(): DateTimeImmutable {
|
|
||||||
return CarbonImmutable::create(2038, 11, 11, 22, 13, 0, 'Europe/Minsk');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
10
api/components/OAuth2/Events/RequestedRefreshToken.php
Normal file
10
api/components/OAuth2/Events/RequestedRefreshToken.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace api\components\OAuth2\Events;
|
||||||
|
|
||||||
|
use League\Event\AbstractEvent;
|
||||||
|
|
||||||
|
class RequestedRefreshToken extends AbstractEvent {
|
||||||
|
|
||||||
|
}
|
@ -4,22 +4,40 @@ declare(strict_types=1);
|
|||||||
namespace api\components\OAuth2\Grants;
|
namespace api\components\OAuth2\Grants;
|
||||||
|
|
||||||
use api\components\OAuth2\CryptTrait;
|
use api\components\OAuth2\CryptTrait;
|
||||||
|
use api\components\OAuth2\Events\RequestedRefreshToken;
|
||||||
use api\components\OAuth2\Repositories\PublicScopeRepository;
|
use api\components\OAuth2\Repositories\PublicScopeRepository;
|
||||||
|
use DateInterval;
|
||||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||||
use League\OAuth2\Server\Grant\AuthCodeGrant as BaseAuthCodeGrant;
|
use League\OAuth2\Server\Grant\AuthCodeGrant as BaseAuthCodeGrant;
|
||||||
|
|
||||||
class AuthCodeGrant extends BaseAuthCodeGrant {
|
class AuthCodeGrant extends BaseAuthCodeGrant {
|
||||||
use CryptTrait;
|
use CryptTrait;
|
||||||
|
|
||||||
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface {
|
/**
|
||||||
foreach ($accessToken->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) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,11 @@ declare(strict_types=1);
|
|||||||
namespace api\components\OAuth2\Grants;
|
namespace api\components\OAuth2\Grants;
|
||||||
|
|
||||||
use api\components\OAuth2\CryptTrait;
|
use api\components\OAuth2\CryptTrait;
|
||||||
|
use api\components\Tokens\TokenReader;
|
||||||
|
use Carbon\Carbon;
|
||||||
use common\models\OauthSession;
|
use common\models\OauthSession;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Lcobucci\JWT\ValidationData;
|
||||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||||
@ -32,7 +36,7 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
|||||||
return $this->validateLegacyRefreshToken($refreshToken);
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,34 +3,25 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace api\components\OAuth2\Repositories;
|
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\Entities\RefreshTokenEntityInterface;
|
||||||
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
|
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
|
|
||||||
class RefreshTokenRepository implements RefreshTokenRepositoryInterface {
|
class RefreshTokenRepository implements RefreshTokenRepositoryInterface {
|
||||||
|
|
||||||
public function getNewRefreshToken(): ?RefreshTokenEntityInterface {
|
public function getNewRefreshToken(): ?RefreshTokenEntityInterface {
|
||||||
return new RefreshTokenEntity();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void {
|
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void {
|
||||||
$model = new OauthRefreshToken();
|
// Do nothing
|
||||||
$model->id = $refreshTokenEntity->getIdentifier();
|
|
||||||
$model->account_id = $refreshTokenEntity->getAccessToken()->getUserIdentifier();
|
|
||||||
$model->client_id = $refreshTokenEntity->getAccessToken()->getClient()->getIdentifier();
|
|
||||||
|
|
||||||
Assert::true($model->save());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revokeRefreshToken($tokenId): void {
|
public function revokeRefreshToken($tokenId): void {
|
||||||
// Currently we're not rotating refresh tokens so do not revoke
|
// Do nothing
|
||||||
// token during any OAuth2 grant
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isRefreshTokenRevoked($tokenId): bool {
|
public function isRefreshTokenRevoked($tokenId): bool {
|
||||||
return OauthRefreshToken::find()->andWhere(['id' => $tokenId])->exists() === false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
64
api/components/Tokens/TokenReader.php
Normal file
64
api/components/Tokens/TokenReader.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace api\components\Tokens;
|
||||||
|
|
||||||
|
use Lcobucci\JWT\Token;
|
||||||
|
use Yii;
|
||||||
|
|
||||||
|
class TokenReader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Token
|
||||||
|
*/
|
||||||
|
private $token;
|
||||||
|
|
||||||
|
public function __construct(Token $token) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -68,7 +68,7 @@ class TokensFactory extends Component {
|
|||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private function prepareScopes(array $scopes): 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) {
|
if ($scope instanceof ScopeEntityInterface) {
|
||||||
return $scope->getIdentifier();
|
return $scope->getIdentifier();
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace api\components\User;
|
namespace api\components\User;
|
||||||
|
|
||||||
use api\components\Tokens\TokensFactory;
|
use api\components\Tokens\TokenReader;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Lcobucci\JWT\Token;
|
use Lcobucci\JWT\Token;
|
||||||
use Lcobucci\JWT\ValidationData;
|
use Lcobucci\JWT\ValidationData;
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
use Yii;
|
use Yii;
|
||||||
use yii\base\NotSupportedException;
|
use yii\base\NotSupportedException;
|
||||||
use yii\web\UnauthorizedHttpException;
|
use yii\web\UnauthorizedHttpException;
|
||||||
@ -21,6 +20,11 @@ class JwtIdentity implements IdentityInterface {
|
|||||||
*/
|
*/
|
||||||
private $token;
|
private $token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TokenReader|null
|
||||||
|
*/
|
||||||
|
private $reader;
|
||||||
|
|
||||||
private function __construct(Token $token) {
|
private function __construct(Token $token) {
|
||||||
$this->token = $token;
|
$this->token = $token;
|
||||||
}
|
}
|
||||||
@ -46,11 +50,6 @@ class JwtIdentity implements IdentityInterface {
|
|||||||
throw new UnauthorizedHttpException('Incorrect token');
|
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);
|
return new self($token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,24 +58,11 @@ class JwtIdentity implements IdentityInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getAccount(): ?Account {
|
public function getAccount(): ?Account {
|
||||||
$subject = $this->token->getClaim('sub', false);
|
return Account::findOne(['id' => $this->getReader()->getAccountId()]);
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAssignedPermissions(): array {
|
public function getAssignedPermissions(): array {
|
||||||
$scopesClaim = $this->token->getClaim('ely-scopes', false);
|
return $this->getReader()->getScopes() ?? [];
|
||||||
if ($scopesClaim === false) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return explode(',', $scopesClaim);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): string {
|
public function getId(): string {
|
||||||
@ -98,4 +84,12 @@ class JwtIdentity implements IdentityInterface {
|
|||||||
|
|
||||||
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreEnd
|
||||||
|
|
||||||
|
private function getReader(): TokenReader {
|
||||||
|
if ($this->reader === null) {
|
||||||
|
$this->reader = new TokenReader($this->token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->reader;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace api\modules\authserver\models;
|
namespace api\modules\authserver\models;
|
||||||
|
|
||||||
|
use api\components\Tokens\TokenReader;
|
||||||
use api\models\base\ApiForm;
|
use api\models\base\ApiForm;
|
||||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||||
use api\modules\authserver\validators\AccessTokenValidator;
|
use api\modules\authserver\validators\AccessTokenValidator;
|
||||||
@ -49,16 +50,12 @@ class RefreshTokenForm extends ApiForm {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$token = Yii::$app->tokens->parse($this->accessToken);
|
$token = Yii::$app->tokens->parse($this->accessToken);
|
||||||
|
$tokenReader = new TokenReader($token);
|
||||||
$encodedClientToken = $token->getClaim('ely-client-token');
|
if ($tokenReader->getMinecraftClientToken() !== $this->clientToken) {
|
||||||
$clientToken = Yii::$app->tokens->decryptValue($encodedClientToken);
|
|
||||||
if ($clientToken !== $this->clientToken) {
|
|
||||||
throw new ForbiddenOperationException('Invalid token.');
|
throw new ForbiddenOperationException('Invalid token.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$accountClaim = $token->getClaim('sub');
|
$account = Account::findOne(['id' => $tokenReader->getAccountId()]);
|
||||||
$accountId = (int)explode('|', $accountClaim)[1];
|
|
||||||
$account = Account::findOne(['id' => $accountId]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($account === null) {
|
if ($account === null) {
|
||||||
|
@ -6,6 +6,8 @@ namespace api\modules\oauth\controllers;
|
|||||||
use api\controllers\Controller;
|
use api\controllers\Controller;
|
||||||
use api\modules\oauth\models\OauthProcess;
|
use api\modules\oauth\models\OauthProcess;
|
||||||
use api\rbac\Permissions as P;
|
use api\rbac\Permissions as P;
|
||||||
|
use GuzzleHttp\Psr7\ServerRequest;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Yii;
|
use Yii;
|
||||||
use yii\filters\AccessControl;
|
use yii\filters\AccessControl;
|
||||||
use yii\helpers\ArrayHelper;
|
use yii\helpers\ArrayHelper;
|
||||||
@ -45,19 +47,23 @@ class AuthorizationController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function actionValidate(): array {
|
public function actionValidate(): array {
|
||||||
return $this->createOauthProcess()->validate();
|
return $this->createOauthProcess()->validate($this->getServerRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function actionComplete(): array {
|
public function actionComplete(): array {
|
||||||
return $this->createOauthProcess()->complete();
|
return $this->createOauthProcess()->complete($this->getServerRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function actionToken(): array {
|
public function actionToken(): array {
|
||||||
return $this->createOauthProcess()->getToken();
|
return $this->createOauthProcess()->getToken($this->getServerRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createOauthProcess(): OauthProcess {
|
private function createOauthProcess(): OauthProcess {
|
||||||
return new OauthProcess(Yii::$app->oauth->getAuthServer());
|
return new OauthProcess(Yii::$app->oauth->getAuthServer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getServerRequest(): ServerRequestInterface {
|
||||||
|
return ServerRequest::fromGlobals();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,12 @@ declare(strict_types=1);
|
|||||||
namespace api\modules\oauth\models;
|
namespace api\modules\oauth\models;
|
||||||
|
|
||||||
use api\components\OAuth2\Entities\UserEntity;
|
use api\components\OAuth2\Entities\UserEntity;
|
||||||
|
use api\components\OAuth2\Events\RequestedRefreshToken;
|
||||||
use api\rbac\Permissions as P;
|
use api\rbac\Permissions as P;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
use common\models\OauthClient;
|
use common\models\OauthClient;
|
||||||
use common\models\OauthSession;
|
use common\models\OauthSession;
|
||||||
use GuzzleHttp\Psr7\Response;
|
use GuzzleHttp\Psr7\Response;
|
||||||
use GuzzleHttp\Psr7\ServerRequest;
|
|
||||||
use League\OAuth2\Server\AuthorizationServer;
|
use League\OAuth2\Server\AuthorizationServer;
|
||||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||||
@ -20,6 +20,7 @@ use Yii;
|
|||||||
|
|
||||||
class OauthProcess {
|
class OauthProcess {
|
||||||
|
|
||||||
|
// TODO: merge this with PublicScopesRepository
|
||||||
private const INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
|
private const INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
|
||||||
P::OBTAIN_OWN_ACCOUNT_INFO => 'account_info',
|
P::OBTAIN_OWN_ACCOUNT_INFO => 'account_info',
|
||||||
P::OBTAIN_ACCOUNT_EMAIL => 'account_email',
|
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.
|
* In addition, you can pass the description value to override the application's description.
|
||||||
*
|
*
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function validate(): array {
|
public function validate(ServerRequestInterface $request): array {
|
||||||
try {
|
try {
|
||||||
$request = $this->getRequest();
|
|
||||||
$authRequest = $this->server->validateAuthorizationRequest($request);
|
$authRequest = $this->server->validateAuthorizationRequest($request);
|
||||||
$client = $authRequest->getClient();
|
$client = $authRequest->getClient();
|
||||||
/** @var OauthClient $clientModel */
|
/** @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.
|
* If the field is present, it will be interpreted as any value resulting in false positives.
|
||||||
* Otherwise, the value will be interpreted as "true".
|
* Otherwise, the value will be interpreted as "true".
|
||||||
*
|
*
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function complete(): array {
|
public function complete(ServerRequestInterface $request): array {
|
||||||
try {
|
try {
|
||||||
Yii::$app->statsd->inc('oauth.complete.attempt');
|
Yii::$app->statsd->inc('oauth.complete.attempt');
|
||||||
|
|
||||||
$request = $this->getRequest();
|
|
||||||
$authRequest = $this->server->validateAuthorizationRequest($request);
|
$authRequest = $this->server->validateAuthorizationRequest($request);
|
||||||
/** @var Account $account */
|
/** @var Account $account */
|
||||||
$account = Yii::$app->user->identity->getAccount();
|
$account = Yii::$app->user->identity->getAccount();
|
||||||
@ -151,18 +152,29 @@ class OauthProcess {
|
|||||||
* grant_type,
|
* grant_type,
|
||||||
* ]
|
* ]
|
||||||
*
|
*
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getToken(): array {
|
public function getToken(ServerRequestInterface $request): array {
|
||||||
$request = $this->getRequest();
|
|
||||||
$params = (array)$request->getParsedBody();
|
$params = (array)$request->getParsedBody();
|
||||||
$clientId = $params['client_id'] ?? '';
|
$clientId = $params['client_id'] ?? '';
|
||||||
$grantType = $params['grant_type'] ?? 'null';
|
$grantType = $params['grant_type'] ?? 'null';
|
||||||
try {
|
try {
|
||||||
Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.attempt");
|
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));
|
$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);
|
$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_client.{$clientId}");
|
||||||
Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.success");
|
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 {
|
private function createAcceptRequiredException(): OAuthServerException {
|
||||||
return new OAuthServerException(
|
return new OAuthServerException(
|
||||||
'Client must accept authentication request.',
|
'Client must accept authentication request.',
|
||||||
|
@ -27,7 +27,7 @@ class RefreshTokenCest {
|
|||||||
'refresh_token' => $refreshToken,
|
'refresh_token' => $refreshToken,
|
||||||
'client_id' => 'ely',
|
'client_id' => 'ely',
|
||||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||||
'scope' => 'minecraft_server_session offline_access',
|
'scope' => 'minecraft_server_session',
|
||||||
]);
|
]);
|
||||||
$this->canSeeRefreshTokenSuccess($I);
|
$this->canSeeRefreshTokenSuccess($I);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ namespace api\tests\unit\components\OAuth2\Entities;
|
|||||||
|
|
||||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||||
use api\tests\unit\TestCase;
|
use api\tests\unit\TestCase;
|
||||||
use DateInterval;
|
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||||
@ -22,35 +21,10 @@ class AccessTokenEntityTest extends TestCase {
|
|||||||
$entity->setExpiryDateTime(new DateTimeImmutable());
|
$entity->setExpiryDateTime(new DateTimeImmutable());
|
||||||
$entity->addScope($this->createScopeEntity('first'));
|
$entity->addScope($this->createScopeEntity('first'));
|
||||||
$entity->addScope($this->createScopeEntity('second'));
|
$entity->addScope($this->createScopeEntity('second'));
|
||||||
$entity->addScope($this->createScopeEntity('offline_access'));
|
|
||||||
|
|
||||||
$token = (string)$entity;
|
$token = (string)$entity;
|
||||||
$payloads = json_decode(base64_decode(explode('.', $token)[1]), true);
|
$payloads = json_decode(base64_decode(explode('.', $token)[1]), true);
|
||||||
$this->assertStringNotContainsString('offline_access', $payloads['ely-scopes']);
|
$this->assertSame('first,second', $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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createScopeEntity(string $id): ScopeEntityInterface {
|
private function createScopeEntity(string $id): ScopeEntityInterface {
|
||||||
|
@ -50,10 +50,6 @@ class JwtIdentityTest extends TestCase {
|
|||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.yth31f2PyhUkYSfBlizzUXWIgOvxxk8gNP-js0z8g1OT5rig40FPTIkgsZRctAwAAlj6QoIWW7-hxLTcSb2vmw',
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.yth31f2PyhUkYSfBlizzUXWIgOvxxk8gNP-js0z8g1OT5rig40FPTIkgsZRctAwAAlj6QoIWW7-hxLTcSb2vmw',
|
||||||
'Incorrect token',
|
'Incorrect token',
|
||||||
];
|
];
|
||||||
yield 'invalid sub' => [
|
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoxMjM0fQ.yigP5nWFdX0ktbuZC_Unb9bWxpAVd7Nv8Fb1Vsa0t5WkVA88VbhPi2P-CenbDOy8ngwoGV9m3c3upMs2V3gqvw',
|
|
||||||
'Incorrect token',
|
|
||||||
];
|
|
||||||
yield 'empty token' => ['', 'Incorrect token'];
|
yield 'empty token' => ['', 'Incorrect token'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +62,10 @@ class JwtIdentityTest extends TestCase {
|
|||||||
$identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDk5OTk5In0.1pAnhkR-_ZqzjLBR-PNIMJUXRSUK3aYixrFNKZg2ynPNPiDvzh8U-iBTT6XRfMP5nvfXZucRpoPVoiXtx40CUQ');
|
$identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDk5OTk5In0.1pAnhkR-_ZqzjLBR-PNIMJUXRSUK3aYixrFNKZg2ynPNPiDvzh8U-iBTT6XRfMP5nvfXZucRpoPVoiXtx40CUQ');
|
||||||
$this->assertNull($identity->getAccount());
|
$this->assertNull($identity->getAccount());
|
||||||
|
|
||||||
|
// Sub contains invalid value
|
||||||
|
$identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoxMjM0fQ.yigP5nWFdX0ktbuZC_Unb9bWxpAVd7Nv8Fb1Vsa0t5WkVA88VbhPi2P-CenbDOy8ngwoGV9m3c3upMs2V3gqvw');
|
||||||
|
$this->assertNull($identity->getAccount());
|
||||||
|
|
||||||
// Token without sub claim
|
// Token without sub claim
|
||||||
$identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Mn0.QxmYgSflZOQmhzYRr8bowU767yu4yKgTVaho0MPuyCmUfZO_0O0SQASMKVILf-wlT0ODTTG7vD753a2MTAmPmw');
|
$identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Mn0.QxmYgSflZOQmhzYRr8bowU767yu4yKgTVaho0MPuyCmUfZO_0O0SQASMKVILf-wlT0ODTTG7vD753a2MTAmPmw');
|
||||||
$this->assertNull($identity->getAccount());
|
$this->assertNull($identity->getAccount());
|
||||||
|
@ -41,7 +41,6 @@ use const common\LATEST_RULES_VERSION;
|
|||||||
* @property UsernameHistory[] $usernameHistory
|
* @property UsernameHistory[] $usernameHistory
|
||||||
* @property AccountSession[] $sessions
|
* @property AccountSession[] $sessions
|
||||||
* @property MinecraftAccessKey[] $minecraftAccessKeys
|
* @property MinecraftAccessKey[] $minecraftAccessKeys
|
||||||
* @property-read OauthRefreshToken[] $oauthRefreshTokens
|
|
||||||
*
|
*
|
||||||
* Behaviors:
|
* Behaviors:
|
||||||
* @mixin TimestampBehavior
|
* @mixin TimestampBehavior
|
||||||
@ -102,10 +101,6 @@ class Account extends ActiveRecord {
|
|||||||
return $this->hasMany(OauthClient::class, ['account_id' => 'id']);
|
return $this->hasMany(OauthClient::class, ['account_id' => 'id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOauthRefreshTokens(): ActiveQuery {
|
|
||||||
return $this->hasMany(OauthRefreshToken::class, ['account_id' => 'id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUsernameHistory(): ActiveQuery {
|
public function getUsernameHistory(): ActiveQuery {
|
||||||
return $this->hasMany(UsernameHistory::class, ['account_id' => 'id']);
|
return $this->hasMany(UsernameHistory::class, ['account_id' => 'id']);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,6 @@ use yii\db\ActiveRecord;
|
|||||||
* Behaviors:
|
* Behaviors:
|
||||||
* @property Account|null $account
|
* @property Account|null $account
|
||||||
* @property OauthSession[] $sessions
|
* @property OauthSession[] $sessions
|
||||||
* @property-read OauthRefreshToken[] $refreshTokens
|
|
||||||
*/
|
*/
|
||||||
class OauthClient extends ActiveRecord {
|
class OauthClient extends ActiveRecord {
|
||||||
|
|
||||||
@ -58,10 +57,6 @@ class OauthClient extends ActiveRecord {
|
|||||||
return $this->hasMany(OauthSession::class, ['client_id' => 'id']);
|
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 {
|
public static function find(): OauthClientQuery {
|
||||||
return Yii::createObject(OauthClientQuery::class, [static::class]);
|
return Yii::createObject(OauthClientQuery::class, [static::class]);
|
||||||
}
|
}
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace common\models;
|
|
||||||
|
|
||||||
use yii\behaviors\TimestampBehavior;
|
|
||||||
use yii\db\ActiveQuery;
|
|
||||||
use yii\db\ActiveRecord;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fields:
|
|
||||||
* @property string $id
|
|
||||||
* @property int $account_id
|
|
||||||
* @property int $client_id
|
|
||||||
* @property int $issued_at
|
|
||||||
*
|
|
||||||
* Relations:
|
|
||||||
* @property-read OauthSession $session
|
|
||||||
* @property-read Account $account
|
|
||||||
* @property-read OauthClient $client
|
|
||||||
*/
|
|
||||||
class OauthRefreshToken extends ActiveRecord {
|
|
||||||
|
|
||||||
public static function tableName(): string {
|
|
||||||
return 'oauth_refresh_tokens';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function behaviors(): array {
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
'class' => 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']);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -19,7 +19,6 @@ use yii\db\ActiveRecord;
|
|||||||
* Relations:
|
* Relations:
|
||||||
* @property-read OauthClient $client
|
* @property-read OauthClient $client
|
||||||
* @property-read Account $account
|
* @property-read Account $account
|
||||||
* @property-read OauthRefreshToken[] $refreshTokens
|
|
||||||
*/
|
*/
|
||||||
class OauthSession extends ActiveRecord {
|
class OauthSession extends ActiveRecord {
|
||||||
|
|
||||||
@ -44,10 +43,6 @@ class OauthSession extends ActiveRecord {
|
|||||||
return $this->hasOne(Account::class, ['id' => 'owner_id']);
|
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 {
|
public function getScopes(): array {
|
||||||
if (empty($this->scopes) && $this->legacy_id !== null) {
|
if (empty($this->scopes) && $this->legacy_id !== null) {
|
||||||
return Yii::$app->redis->smembers($this->getLegacyRedisScopesKey());
|
return Yii::$app->redis->smembers($this->getLegacyRedisScopesKey());
|
||||||
|
@ -55,7 +55,6 @@ class FixtureHelper extends Module {
|
|||||||
'legacyOauthSessionsScopes' => fixtures\LegacyOauthSessionScopeFixtures::class,
|
'legacyOauthSessionsScopes' => fixtures\LegacyOauthSessionScopeFixtures::class,
|
||||||
'legacyOauthAccessTokens' => fixtures\LegacyOauthAccessTokenFixture::class,
|
'legacyOauthAccessTokens' => fixtures\LegacyOauthAccessTokenFixture::class,
|
||||||
'legacyOauthAccessTokensScopes' => fixtures\LegacyOauthAccessTokenScopeFixture::class,
|
'legacyOauthAccessTokensScopes' => fixtures\LegacyOauthAccessTokenScopeFixture::class,
|
||||||
'oauthRefreshTokens' => fixtures\OauthRefreshTokensFixture::class,
|
|
||||||
'legacyOauthRefreshTokens' => fixtures\LegacyOauthRefreshTokenFixture::class,
|
'legacyOauthRefreshTokens' => fixtures\LegacyOauthRefreshTokenFixture::class,
|
||||||
'minecraftAccessKeys' => fixtures\MinecraftAccessKeyFixture::class,
|
'minecraftAccessKeys' => fixtures\MinecraftAccessKeyFixture::class,
|
||||||
];
|
];
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace common\tests\fixtures;
|
|
||||||
|
|
||||||
use common\models\OauthRefreshToken;
|
|
||||||
use yii\test\ActiveFixture;
|
|
||||||
|
|
||||||
class OauthRefreshTokensFixture extends ActiveFixture {
|
|
||||||
|
|
||||||
public $modelClass = OauthRefreshToken::class;
|
|
||||||
|
|
||||||
public $dataFile = '@root/common/tests/fixtures/data/oauth-refresh-tokens.php';
|
|
||||||
|
|
||||||
public $depends = [
|
|
||||||
OauthSessionFixture::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
}
|
|
@ -34,20 +34,9 @@ class m190914_181236_rework_oauth_related_tables extends Migration {
|
|||||||
$this->addForeignKey('FK_oauth_session_to_account', 'oauth_sessions', 'account_id', 'accounts', 'id', 'CASCADE', 'CASCADE');
|
$this->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->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->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() {
|
public function safeDown() {
|
||||||
$this->dropTable('oauth_refresh_tokens');
|
|
||||||
|
|
||||||
$this->dropColumn('oauth_sessions', 'scopes');
|
$this->dropColumn('oauth_sessions', 'scopes');
|
||||||
$this->dropForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions');
|
$this->dropForeignKey('FK_oauth_session_to_oauth_client', 'oauth_sessions');
|
||||||
$this->dropForeignKey('FK_oauth_session_to_account', 'oauth_sessions');
|
$this->dropForeignKey('FK_oauth_session_to_account', 'oauth_sessions');
|
||||||
|
Loading…
Reference in New Issue
Block a user