From cf62c686b19191a7f02622e87e07a390123c9adb Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Sun, 22 Sep 2019 18:42:21 +0300 Subject: [PATCH] Rework identity provider for the legacy OAuth2 tokens [skip ci] --- api/components/OAuth2/Component.php | 4 - .../Repositories/AccessTokenStorage.php | 70 ----------- api/components/User/IdentityFactory.php | 21 ++-- api/components/User/LegacyOAuth2Identity.php | 119 ++++++++++++++++++ api/components/User/OAuth2Identity.php | 78 ------------ .../unit/components/User/ComponentTest.php | 4 +- .../components/User/IdentityFactoryTest.php | 4 +- ...yTest.php => LegacyOAuth2IdentityTest.php} | 12 +- common/components/Redis/Key.php | 63 ---------- common/components/Redis/Set.php | 50 -------- 10 files changed, 141 insertions(+), 284 deletions(-) delete mode 100644 api/components/OAuth2/Repositories/AccessTokenStorage.php create mode 100644 api/components/User/LegacyOAuth2Identity.php delete mode 100644 api/components/User/OAuth2Identity.php rename api/tests/unit/components/User/{OAuth2IdentityTest.php => LegacyOAuth2IdentityTest.php} (82%) delete mode 100644 common/components/Redis/Key.php delete mode 100644 common/components/Redis/Set.php diff --git a/api/components/OAuth2/Component.php b/api/components/OAuth2/Component.php index 93656bb..10350f0 100644 --- a/api/components/OAuth2/Component.php +++ b/api/components/OAuth2/Component.php @@ -11,9 +11,6 @@ use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Grant; use yii\base\Component as BaseComponent; -/** - * @property AuthorizationServer $authServer - */ class Component extends BaseComponent { /** @@ -39,7 +36,6 @@ class Component extends BaseComponent { new EmptyKey(), '123' // TODO: extract to the variable ); - /** @noinspection PhpUnhandledExceptionInspection */ $authCodeGrant = new AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M')); $authCodeGrant->disableRequireCodeChallengeForPublicClients(); $authServer->enableGrantType($authCodeGrant, $accessTokenTTL); diff --git a/api/components/OAuth2/Repositories/AccessTokenStorage.php b/api/components/OAuth2/Repositories/AccessTokenStorage.php deleted file mode 100644 index bfe6765..0000000 --- a/api/components/OAuth2/Repositories/AccessTokenStorage.php +++ /dev/null @@ -1,70 +0,0 @@ -dataTable, $token))->getValue()); - if ($result === null) { - return null; - } - - $token = new AccessTokenEntity($this->server); - $token->setId($result['id']); - $token->setExpireTime($result['expire_time']); - $token->setSessionId($result['session_id']); - - return $token; - } - - public function getScopes(OriginalAccessTokenEntity $token) { - $scopes = $this->scopes($token->getId()); - $entities = []; - foreach ($scopes as $scope) { - if ($this->server->getScopeStorage()->get($scope) !== null) { - $entities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]); - } - } - - return $entities; - } - - public function create($token, $expireTime, $sessionId) { - $payload = Json::encode([ - 'id' => $token, - 'expire_time' => $expireTime, - 'session_id' => $sessionId, - ]); - - $this->key($token)->setValue($payload)->expireAt($expireTime); - } - - public function associateScope(OriginalAccessTokenEntity $token, ScopeEntity $scope) { - $this->scopes($token->getId())->add($scope->getId())->expireAt($token->getExpireTime()); - } - - public function delete(OriginalAccessTokenEntity $token) { - $this->key($token->getId())->delete(); - $this->scopes($token->getId())->delete(); - } - - private function key(string $token): Key { - return new Key($this->dataTable, $token); - } - - private function scopes(string $token): Set { - return new Set($this->dataTable, $token, 'scopes'); - } - -} diff --git a/api/components/User/IdentityFactory.php b/api/components/User/IdentityFactory.php index 2b59630..bf53c19 100644 --- a/api/components/User/IdentityFactory.php +++ b/api/components/User/IdentityFactory.php @@ -8,19 +8,24 @@ use yii\web\UnauthorizedHttpException; class IdentityFactory { /** - * @throws UnauthorizedHttpException + * @param string $token + * @param string $type + * * @return IdentityInterface + * @throws UnauthorizedHttpException */ public static function findIdentityByAccessToken($token, $type = null): IdentityInterface { - if (empty($token)) { - throw new UnauthorizedHttpException('Incorrect token'); + if (!empty($token)) { + if (mb_strlen($token) === 40) { + return LegacyOAuth2Identity::findIdentityByAccessToken($token, $type); + } + + if (substr_count($token, '.') === 2) { + return JwtIdentity::findIdentityByAccessToken($token, $type); + } } - if (substr_count($token, '.') === 2) { - return JwtIdentity::findIdentityByAccessToken($token, $type); - } - - return OAuth2Identity::findIdentityByAccessToken($token, $type); + throw new UnauthorizedHttpException('Incorrect token'); } } diff --git a/api/components/User/LegacyOAuth2Identity.php b/api/components/User/LegacyOAuth2Identity.php new file mode 100644 index 0000000..422a460 --- /dev/null +++ b/api/components/User/LegacyOAuth2Identity.php @@ -0,0 +1,119 @@ +accessToken = $accessToken; + $this->sessionId = $sessionId; + $this->scopes = $scopes; + } + + /** + * @inheritdoc + * @throws UnauthorizedHttpException + * @return IdentityInterface + */ + public static function findIdentityByAccessToken($token, $type = null): IdentityInterface { + $tokenParams = self::findRecordOnLegacyStorage($token); + if ($tokenParams === null) { + throw new UnauthorizedHttpException('Incorrect token'); + } + + if ($tokenParams['expire_time'] < time()) { + throw new UnauthorizedHttpException('Token expired'); + } + + return new static($token, $tokenParams['session_id'], $tokenParams['scopes']); + } + + public function getAccount(): ?Account { + $session = $this->getSession(); + if ($session === null) { + return null; + } + + return $session->account; + } + + /** + * @return string[] + */ + public function getAssignedPermissions(): array { + return $this->scopes; + } + + public function getId(): string { + return $this->accessToken; + } + + // @codeCoverageIgnoreStart + public function getAuthKey() { + throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth'); + } + + public function validateAuthKey($authKey) { + throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth'); + } + + public static function findIdentity($id) { + throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth'); + } + + // @codeCoverageIgnoreEnd + + private static function findRecordOnLegacyStorage(string $accessToken): ?array { + $record = Yii::$app->redis->get("oauth:access:tokens:{$accessToken}"); + if ($record === null) { + return null; + } + + try { + $data = json_decode($record, true, 512, JSON_THROW_ON_ERROR); + } catch (Exception $e) { + return null; + } + + $data['scopes'] = (array)Yii::$app->redis->smembers("oauth:access:tokens:{$accessToken}:scopes"); + + return $data; + } + + private function getSession(): ?OauthSession { + if ($this->session === false) { + $this->session = OauthSession::findOne(['id' => $this->sessionId]); + } + + return $this->session; + } + +} diff --git a/api/components/User/OAuth2Identity.php b/api/components/User/OAuth2Identity.php deleted file mode 100644 index 63acd81..0000000 --- a/api/components/User/OAuth2Identity.php +++ /dev/null @@ -1,78 +0,0 @@ -_accessToken = $accessToken; - } - - /** - * @inheritdoc - * @throws UnauthorizedHttpException - * @return IdentityInterface - */ - public static function findIdentityByAccessToken($token, $type = null): IdentityInterface { - /** @var AccessTokenEntity|null $model */ - // TODO: rework - $model = Yii::$app->oauth->getAccessTokenStorage()->get($token); - if ($model === null) { - throw new UnauthorizedHttpException('Incorrect token'); - } - - if ($model->isExpired()) { - throw new UnauthorizedHttpException('Token expired'); - } - - return new static($model); - } - - public function getAccount(): ?Account { - return $this->getSession()->account; - } - - /** - * @return string[] - */ - public function getAssignedPermissions(): array { - return array_keys($this->_accessToken->getScopes()); - } - - public function getId(): string { - return $this->_accessToken->getId(); - } - - // @codeCoverageIgnoreStart - public function getAuthKey() { - throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth'); - } - - public function validateAuthKey($authKey) { - throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth'); - } - - public static function findIdentity($id) { - throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth'); - } - - // @codeCoverageIgnoreEnd - - private function getSession(): OauthSession { - return OauthSession::findOne(['id' => $this->_accessToken->getSessionId()]); - } - -} diff --git a/api/tests/unit/components/User/ComponentTest.php b/api/tests/unit/components/User/ComponentTest.php index e9ca1cc..1a2fea5 100644 --- a/api/tests/unit/components/User/ComponentTest.php +++ b/api/tests/unit/components/User/ComponentTest.php @@ -5,7 +5,7 @@ namespace codeception\api\unit\components\User; use api\components\User\Component; use api\components\User\JwtIdentity; -use api\components\User\OAuth2Identity; +use api\components\User\LegacyOAuth2Identity; use api\tests\unit\TestCase; use common\models\Account; use common\models\AccountSession; @@ -41,7 +41,7 @@ class ComponentTest extends TestCase { $this->assertNull($component->getActiveSession()); // Identity is a Oauth2Identity - $component->setIdentity(mock(OAuth2Identity::class)); + $component->setIdentity(mock(LegacyOAuth2Identity::class)); $this->assertNull($component->getActiveSession()); // Identity is correct, but have no jti claim diff --git a/api/tests/unit/components/User/IdentityFactoryTest.php b/api/tests/unit/components/User/IdentityFactoryTest.php index b3d6851..899f364 100644 --- a/api/tests/unit/components/User/IdentityFactoryTest.php +++ b/api/tests/unit/components/User/IdentityFactoryTest.php @@ -7,7 +7,7 @@ use api\components\OAuth2\Component; use api\components\OAuth2\Entities\AccessTokenEntity; use api\components\User\IdentityFactory; use api\components\User\JwtIdentity; -use api\components\User\OAuth2Identity; +use api\components\User\LegacyOAuth2Identity; use api\tests\unit\TestCase; use Carbon\Carbon; use League\OAuth2\Server\AbstractServer; @@ -37,7 +37,7 @@ class IdentityFactoryTest extends TestCase { Yii::$app->set('oauth', $component); $identity = IdentityFactory::findIdentityByAccessToken('mock-token'); - $this->assertInstanceOf(OAuth2Identity::class, $identity); + $this->assertInstanceOf(LegacyOAuth2Identity::class, $identity); } public function testFindIdentityByAccessTokenWithEmptyValue() { diff --git a/api/tests/unit/components/User/OAuth2IdentityTest.php b/api/tests/unit/components/User/LegacyOAuth2IdentityTest.php similarity index 82% rename from api/tests/unit/components/User/OAuth2IdentityTest.php rename to api/tests/unit/components/User/LegacyOAuth2IdentityTest.php index 790f139..e19804d 100644 --- a/api/tests/unit/components/User/OAuth2IdentityTest.php +++ b/api/tests/unit/components/User/LegacyOAuth2IdentityTest.php @@ -5,14 +5,12 @@ namespace api\tests\unit\components\User; use api\components\OAuth2\Component; use api\components\OAuth2\Entities\AccessTokenEntity; -use api\components\User\OAuth2Identity; +use api\components\User\LegacyOAuth2Identity; use api\tests\unit\TestCase; -use League\OAuth2\Server\AbstractServer; -use League\OAuth2\Server\Storage\AccessTokenInterface; use Yii; use yii\web\UnauthorizedHttpException; -class OAuth2IdentityTest extends TestCase { +class LegacyOAuth2IdentityTest extends TestCase { public function testFindIdentityByAccessToken() { $accessToken = new AccessTokenEntity(mock(AbstractServer::class)); @@ -20,7 +18,7 @@ class OAuth2IdentityTest extends TestCase { $accessToken->setId('mock-token'); $this->mockFoundedAccessToken($accessToken); - $identity = OAuth2Identity::findIdentityByAccessToken('mock-token'); + $identity = LegacyOAuth2Identity::findIdentityByAccessToken('mock-token'); $this->assertSame('mock-token', $identity->getId()); } @@ -28,7 +26,7 @@ class OAuth2IdentityTest extends TestCase { $this->expectException(UnauthorizedHttpException::class); $this->expectExceptionMessage('Incorrect token'); - OAuth2Identity::findIdentityByAccessToken('not exists token'); + LegacyOAuth2Identity::findIdentityByAccessToken('not exists token'); } public function testFindIdentityByAccessTokenWithExpiredToken() { @@ -39,7 +37,7 @@ class OAuth2IdentityTest extends TestCase { $accessToken->setExpireTime(time() - 3600); $this->mockFoundedAccessToken($accessToken); - OAuth2Identity::findIdentityByAccessToken('mock-token'); + LegacyOAuth2Identity::findIdentityByAccessToken('mock-token'); } private function mockFoundedAccessToken(AccessTokenEntity $accessToken) { diff --git a/common/components/Redis/Key.php b/common/components/Redis/Key.php deleted file mode 100644 index c8b3be5..0000000 --- a/common/components/Redis/Key.php +++ /dev/null @@ -1,63 +0,0 @@ -key = $this->buildKey($key); - } - - public function getKey(): string { - return $this->key; - } - - public function getValue() { - return Yii::$app->redis->get($this->key); - } - - public function setValue($value): self { - Yii::$app->redis->set($this->key, $value); - return $this; - } - - public function delete(): self { - Yii::$app->redis->del($this->getKey()); - return $this; - } - - public function exists(): bool { - return (bool)Yii::$app->redis->exists($this->key); - } - - public function expire(int $ttl): self { - Yii::$app->redis->expire($this->key, $ttl); - return $this; - } - - public function expireAt(int $unixTimestamp): self { - Yii::$app->redis->expireat($this->key, $unixTimestamp); - return $this; - } - - private function buildKey(array $parts): string { - $keyParts = []; - foreach ($parts as $part) { - $keyParts[] = str_replace('_', ':', $part); - } - - return implode(':', $keyParts); - } - -} diff --git a/common/components/Redis/Set.php b/common/components/Redis/Set.php deleted file mode 100644 index 5023aea..0000000 --- a/common/components/Redis/Set.php +++ /dev/null @@ -1,50 +0,0 @@ -redis->sadd($this->getKey(), $value); - return $this; - } - - public function remove($value): self { - Yii::$app->redis->srem($this->getKey(), $value); - return $this; - } - - public function members(): array { - return Yii::$app->redis->smembers($this->getKey()); - } - - public function getValue(): array { - return $this->members(); - } - - public function exists(string $value = null): bool { - if ($value === null) { - return parent::exists(); - } - - return (bool)Yii::$app->redis->sismember($this->getKey(), $value); - } - - public function diff(array $sets): array { - return Yii::$app->redis->sdiff([$this->getKey(), implode(' ', $sets)]); - } - - /** - * @inheritdoc - */ - public function getIterator() { - return new ArrayIterator($this->members()); - } - -}