mirror of
				https://github.com/elyby/accounts.git
				synced 2025-05-31 14:11:46 +05:30 
			
		
		
		
	Rework identity provider for the legacy OAuth2 tokens [skip ci]
This commit is contained in:
		| @@ -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); | ||||
|   | ||||
| @@ -1,70 +0,0 @@ | ||||
| <?php | ||||
| namespace api\components\OAuth2\Repositories; | ||||
|  | ||||
| use api\components\OAuth2\Entities\AccessTokenEntity; | ||||
| use common\components\Redis\Key; | ||||
| use common\components\Redis\Set; | ||||
| use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity; | ||||
| use League\OAuth2\Server\Entity\ScopeEntity; | ||||
| use League\OAuth2\Server\Storage\AbstractStorage; | ||||
| use League\OAuth2\Server\Storage\AccessTokenInterface; | ||||
| use yii\helpers\Json; | ||||
|  | ||||
| class AccessTokenStorage extends AbstractStorage implements AccessTokenInterface { | ||||
|  | ||||
|     public $dataTable = 'oauth_access_tokens'; | ||||
|  | ||||
|     public function get($token) { | ||||
|         $result = Json::decode((new Key($this->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'); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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'); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										119
									
								
								api/components/User/LegacyOAuth2Identity.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								api/components/User/LegacyOAuth2Identity.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| <?php | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace api\components\User; | ||||
|  | ||||
| use common\models\Account; | ||||
| use common\models\OauthSession; | ||||
| use Exception; | ||||
| use Yii; | ||||
| use yii\base\NotSupportedException; | ||||
| use yii\web\UnauthorizedHttpException; | ||||
|  | ||||
| class LegacyOAuth2Identity implements IdentityInterface { | ||||
|  | ||||
|     /** | ||||
|      * @var string | ||||
|      */ | ||||
|     private $accessToken; | ||||
|  | ||||
|     /** | ||||
|      * @var string | ||||
|      */ | ||||
|     private $sessionId; | ||||
|  | ||||
|     /** | ||||
|      * @var string[] | ||||
|      */ | ||||
|     private $scopes; | ||||
|  | ||||
|     /** | ||||
|      * @var OauthSession|null | ||||
|      */ | ||||
|     private $session = false; | ||||
|  | ||||
|     private function __construct(string $accessToken, string $sessionId, array $scopes) { | ||||
|         $this->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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,78 +0,0 @@ | ||||
| <?php | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace api\components\User; | ||||
|  | ||||
| use api\components\OAuth2\Entities\AccessTokenEntity; | ||||
| use common\models\Account; | ||||
| use common\models\OauthSession; | ||||
| use Yii; | ||||
| use yii\base\NotSupportedException; | ||||
| use yii\web\UnauthorizedHttpException; | ||||
|  | ||||
| class OAuth2Identity implements IdentityInterface { | ||||
|  | ||||
|     /** | ||||
|      * @var AccessTokenEntity | ||||
|      */ | ||||
|     private $_accessToken; | ||||
|  | ||||
|     private function __construct(AccessTokenEntity $accessToken) { | ||||
|         $this->_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()]); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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) { | ||||
		Reference in New Issue
	
	Block a user