mirror of
https://github.com/elyby/accounts.git
synced 2024-11-26 16:52:02 +05:30
Rework identity provider for the legacy OAuth2 tokens [skip ci]
This commit is contained in:
parent
c722c46ad5
commit
cf62c686b1
@ -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) {
|
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
namespace common\components\Redis;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class Key {
|
||||
|
||||
private $key;
|
||||
|
||||
public function __construct(...$key) {
|
||||
if (empty($key)) {
|
||||
throw new InvalidArgumentException('You must specify at least one key.');
|
||||
}
|
||||
|
||||
$this->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);
|
||||
}
|
||||
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
namespace common\components\Redis;
|
||||
|
||||
use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class Set extends Key implements IteratorAggregate {
|
||||
|
||||
public function add($value): self {
|
||||
Yii::$app->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());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user