mirror of
https://github.com/elyby/accounts.git
synced 2024-11-09 15:02:19 +05:30
Add support for the legacy refresh tokens, make the new refresh tokens non-expire [skip ci]
This commit is contained in:
parent
5536c34b9c
commit
c722c46ad5
@ -45,11 +45,11 @@ class Component extends BaseComponent {
|
||||
$authServer->enableGrantType($authCodeGrant, $accessTokenTTL);
|
||||
$authCodeGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||
|
||||
// TODO: extends refresh token life time to forever
|
||||
$refreshTokenGrant = new RefreshTokenGrant($refreshTokensRepo);
|
||||
$authServer->enableGrantType($refreshTokenGrant, $accessTokenTTL);
|
||||
$authServer->enableGrantType($refreshTokenGrant);
|
||||
$refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||
|
||||
// TODO: make these access tokens live longer
|
||||
$clientCredentialsGrant = new Grant\ClientCredentialsGrant();
|
||||
$authServer->enableGrantType($clientCredentialsGrant, $accessTokenTTL);
|
||||
$clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling
|
||||
|
@ -3,6 +3,8 @@ 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;
|
||||
@ -11,4 +13,17 @@ 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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,12 +3,36 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use common\models\OauthSession;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Grant\RefreshTokenGrant as BaseRefreshTokenGrant;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Yii;
|
||||
|
||||
class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
|
||||
/**
|
||||
* Previously, refresh tokens was stored in Redis.
|
||||
* If received refresh token is matches the legacy token template,
|
||||
* restore the information from the legacy storage.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param string $clientId
|
||||
*
|
||||
* @return array
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId): array {
|
||||
$refreshToken = $this->getRequestParameter('refresh_token', $request);
|
||||
if ($refreshToken !== null && mb_strlen($refreshToken) === 40) {
|
||||
return $this->validateLegacyRefreshToken($refreshToken);
|
||||
}
|
||||
|
||||
return parent::validateOldRefreshToken($request, $clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently we're not rotating refresh tokens.
|
||||
* So we overriding this method to always return null, which means,
|
||||
@ -22,4 +46,35 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
return null;
|
||||
}
|
||||
|
||||
private function validateLegacyRefreshToken(string $refreshToken): array {
|
||||
$result = Yii::$app->redis->get("oauth:refresh:tokens:{$refreshToken}");
|
||||
if ($result === null) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
|
||||
}
|
||||
|
||||
try {
|
||||
[
|
||||
'access_token_id' => $accessTokenId,
|
||||
'session_id' => $sessionId,
|
||||
] = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\Exception $e) {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
|
||||
}
|
||||
|
||||
/** @var OauthSession|null $relatedSession */
|
||||
$relatedSession = OauthSession::findOne(['legacy_id' => $sessionId]);
|
||||
if ($relatedSession === null) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
|
||||
}
|
||||
|
||||
return [
|
||||
'client_id' => $relatedSession->client_id,
|
||||
'refresh_token_id' => $refreshToken,
|
||||
'access_token_id' => $accessTokenId,
|
||||
'scopes' => $relatedSession->getScopes(),
|
||||
'user_id' => $relatedSession->account_id,
|
||||
'expire_time' => null,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\RefreshTokenEntity;
|
||||
use common\components\Redis\Key;
|
||||
use common\components\Redis\Set;
|
||||
use common\models\OauthSession;
|
||||
use ErrorException;
|
||||
use League\OAuth2\Server\Entity\RefreshTokenEntity as OriginalRefreshTokenEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\RefreshTokenInterface;
|
||||
use Yii;
|
||||
use yii\helpers\Json;
|
||||
|
||||
class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterface {
|
||||
|
||||
public $dataTable = 'oauth_refresh_tokens';
|
||||
|
||||
public function get($token) {
|
||||
$result = Json::decode((new Key($this->dataTable, $token))->getValue());
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = new RefreshTokenEntity($this->server);
|
||||
$entity->setId($result['id']);
|
||||
$entity->setAccessTokenId($result['access_token_id']);
|
||||
$entity->setSessionId($result['session_id']);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function create($token, $expireTime, $accessToken) {
|
||||
$sessionId = $this->server->getAccessTokenStorage()->get($accessToken)->getSession()->getId();
|
||||
$payload = Json::encode([
|
||||
'id' => $token,
|
||||
'access_token_id' => $accessToken,
|
||||
'session_id' => $sessionId,
|
||||
]);
|
||||
|
||||
$this->key($token)->setValue($payload);
|
||||
$this->sessionHash($sessionId)->add($token);
|
||||
}
|
||||
|
||||
public function delete(OriginalRefreshTokenEntity $token) {
|
||||
if (!$token instanceof RefreshTokenEntity) {
|
||||
throw new ErrorException('Token must be instance of ' . RefreshTokenEntity::class);
|
||||
}
|
||||
|
||||
$this->key($token->getId())->delete();
|
||||
$this->sessionHash($token->getSessionId())->remove($token->getId());
|
||||
}
|
||||
|
||||
public function sessionHash(string $sessionId): Set {
|
||||
$tableName = Yii::$app->db->getSchema()->getRawTableName(OauthSession::tableName());
|
||||
return new Set($tableName, $sessionId, 'refresh_tokens');
|
||||
}
|
||||
|
||||
private function key(string $token): Key {
|
||||
return new Key($this->dataTable, $token);
|
||||
}
|
||||
|
||||
}
|
@ -57,7 +57,7 @@ class AuthorizationController extends Controller {
|
||||
}
|
||||
|
||||
private function createOauthProcess(): OauthProcess {
|
||||
return new OauthProcess(Yii::$app->oauth->authServer);
|
||||
return new OauthProcess(Yii::$app->oauth->getAuthServer());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -56,6 +56,20 @@ class OauthSession extends ActiveRecord {
|
||||
return (array)$this->scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the early period of the project existence, the refresh tokens related to the current session
|
||||
* were stored in Redis. This method allows to get a list of these tokens.
|
||||
*
|
||||
* @return array of refresh tokens (ids)
|
||||
*/
|
||||
public function getLegacyRefreshTokens(): array {
|
||||
if ($this->legacy_id === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Yii::$app->redis->smembers($this->getLegacyRedisRefreshTokensKey());
|
||||
}
|
||||
|
||||
public function beforeDelete(): bool {
|
||||
if (!parent::beforeDelete()) {
|
||||
return false;
|
||||
@ -63,6 +77,7 @@ class OauthSession extends ActiveRecord {
|
||||
|
||||
if ($this->legacy_id !== null) {
|
||||
Yii::$app->redis->del($this->getLegacyRedisScopesKey());
|
||||
Yii::$app->redis->del($this->getLegacyRedisRefreshTokensKey());
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -72,4 +87,8 @@ class OauthSession extends ActiveRecord {
|
||||
return "oauth:sessions:{$this->legacy_id}:scopes";
|
||||
}
|
||||
|
||||
private function getLegacyRedisRefreshTokensKey(): string {
|
||||
return "oauth:sessions:{$this->legacy_id}:refresh:tokens";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ class m190914_181236_rework_oauth_related_tables extends Migration {
|
||||
// Change type again to make column nullable
|
||||
$this->alterColumn('oauth_sessions', 'id', $this->integer(11)->unsigned()->after('client_id'));
|
||||
$this->renameColumn('oauth_sessions', 'id', 'legacy_id');
|
||||
$this->createIndex('legacy_id', 'oauth_sessions', 'legacy_id', true);
|
||||
$this->addPrimaryKey('id', 'oauth_sessions', ['account_id', 'client_id']);
|
||||
$this->dropForeignKey('FK_oauth_session_to_client', 'oauth_sessions');
|
||||
$this->dropIndex('FK_oauth_session_to_client', 'oauth_sessions');
|
||||
@ -53,6 +54,7 @@ class m190914_181236_rework_oauth_related_tables extends Migration {
|
||||
$this->dropIndex('FK_oauth_session_to_oauth_client', 'oauth_sessions');
|
||||
$this->dropPrimaryKey('PRIMARY', 'oauth_sessions');
|
||||
$this->delete('oauth_sessions', ['legacy_id' => null]);
|
||||
$this->dropIndex('legacy_id', 'oauth_sessions');
|
||||
$this->alterColumn('oauth_sessions', 'legacy_id', $this->integer(11)->unsigned()->notNull()->append('AUTO_INCREMENT PRIMARY KEY FIRST'));
|
||||
$this->renameColumn('oauth_sessions', 'legacy_id', 'id');
|
||||
$this->alterColumn('oauth_sessions', 'client_id', $this->db->getTableSchema('oauth_clients')->getColumn('id')->dbType);
|
||||
|
Loading…
Reference in New Issue
Block a user