Merge branch 'oauth2_restructure' into develop

This commit is contained in:
ErickSkrauch 2016-11-30 14:20:45 +03:00
commit ce18d07ae2
46 changed files with 1248 additions and 518 deletions

View File

@ -11,7 +11,7 @@ RUN chmod 400 ~/.ssh/id_rsa \
&& eval $(ssh-agent -s) \
&& ssh-add /root/.ssh/id_rsa \
&& touch /root/.ssh/known_hosts \
&& ssh-keyscan gitlab.com >> /root/.ssh/known_hosts
&& ssh-keyscan gitlab.com gitlab.ely.by >> /root/.ssh/known_hosts
# Копируем composer.json в родительскую директорию, которая не будет синкаться с хостом через
# volume на dev окружении. В entrypoint эта папка будет скопирована обратно.

View File

@ -11,7 +11,7 @@ RUN chmod 400 ~/.ssh/id_rsa \
&& eval $(ssh-agent -s) \
&& ssh-add /root/.ssh/id_rsa \
&& touch /root/.ssh/known_hosts \
&& ssh-keyscan gitlab.com >> /root/.ssh/known_hosts
&& ssh-keyscan gitlab.com gitlab.ely.by >> /root/.ssh/known_hosts
# Копируем composer.json в родительскую директорию, которая не будет синкаться с хостом через
# volume на dev окружении. В entrypoint эта папка будет скопирована обратно.

View File

@ -1,7 +1,7 @@
<?php
namespace api\components\ApiUser;
use common\models\OauthAccessToken;
use Yii;
use yii\rbac\CheckAccessInterface;
class AuthChecker implements CheckAccessInterface {
@ -10,13 +10,12 @@ class AuthChecker implements CheckAccessInterface {
* @inheritdoc
*/
public function checkAccess($token, $permissionName, $params = []) : bool {
/** @var OauthAccessToken|null $accessToken */
$accessToken = OauthAccessToken::findOne($token);
$accessToken = Yii::$app->oauth->getAuthServer()->getAccessTokenStorage()->get($token);
if ($accessToken === null) {
return false;
}
return $accessToken->getScopes()->exists($permissionName);
return $accessToken->hasScope($permissionName);
}
}

View File

@ -1,10 +1,11 @@
<?php
namespace api\components\ApiUser;
use api\components\OAuth2\Entities\AccessTokenEntity;
use common\models\Account;
use common\models\OauthAccessToken;
use common\models\OauthClient;
use common\models\OauthSession;
use Yii;
use yii\base\NotSupportedException;
use yii\web\IdentityInterface;
use yii\web\UnauthorizedHttpException;
@ -13,12 +14,12 @@ use yii\web\UnauthorizedHttpException;
* @property Account $account
* @property OauthClient $client
* @property OauthSession $session
* @property OauthAccessToken $accessToken
* @property AccessTokenEntity $accessToken
*/
class Identity implements IdentityInterface {
/**
* @var OauthAccessToken
* @var AccessTokenEntity
*/
private $_accessToken;
@ -26,8 +27,7 @@ class Identity implements IdentityInterface {
* @inheritdoc
*/
public static function findIdentityByAccessToken($token, $type = null) {
/** @var OauthAccessToken|null $model */
$model = OauthAccessToken::findOne($token);
$model = Yii::$app->oauth->getAuthServer()->getAccessTokenStorage()->get($token);
if ($model === null) {
throw new UnauthorizedHttpException('Incorrect token');
} elseif ($model->isExpired()) {
@ -37,7 +37,7 @@ class Identity implements IdentityInterface {
return new static($model);
}
private function __construct(OauthAccessToken $accessToken) {
private function __construct(AccessTokenEntity $accessToken) {
$this->_accessToken = $accessToken;
}
@ -50,20 +50,20 @@ class Identity implements IdentityInterface {
}
public function getSession() : OauthSession {
return $this->_accessToken->session;
return OauthSession::findOne($this->_accessToken->getSessionId());
}
public function getAccessToken() : OauthAccessToken {
public function getAccessToken() : AccessTokenEntity {
return $this->_accessToken;
}
/**
* Этот метод используется для получения пользователя, к которому привязаны права.
* Этот метод используется для получения токена, к которому привязаны права.
* У нас права привязываются к токенам, так что возвращаем именно его id.
* @inheritdoc
*/
public function getId() {
return $this->_accessToken->access_token;
return $this->_accessToken->getId();
}
public function getAuthKey() {

View File

@ -1,13 +1,13 @@
<?php
namespace common\components\oauth;
namespace api\components\OAuth2;
use common\components\oauth\Storage\Redis\AuthCodeStorage;
use common\components\oauth\Storage\Redis\RefreshTokenStorage;
use common\components\oauth\Storage\Yii2\AccessTokenStorage;
use common\components\oauth\Storage\Yii2\ClientStorage;
use common\components\oauth\Storage\Yii2\ScopeStorage;
use common\components\oauth\Storage\Yii2\SessionStorage;
use common\components\oauth\Util\KeyAlgorithm\UuidAlgorithm;
use api\components\OAuth2\Storage\AuthCodeStorage;
use api\components\OAuth2\Storage\RefreshTokenStorage;
use api\components\OAuth2\Storage\AccessTokenStorage;
use api\components\OAuth2\Storage\ClientStorage;
use api\components\OAuth2\Storage\ScopeStorage;
use api\components\OAuth2\Storage\SessionStorage;
use api\components\OAuth2\Utils\KeyAlgorithm\UuidAlgorithm;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Grant;
use League\OAuth2\Server\Util\SecureKey;
@ -41,22 +41,22 @@ class Component extends \yii\base\Component {
public function getAuthServer() {
if ($this->_authServer === null) {
$authServer = new AuthorizationServer();
$authServer
->setAccessTokenStorage(new AccessTokenStorage())
->setClientStorage(new ClientStorage())
->setScopeStorage(new ScopeStorage())
->setSessionStorage(new SessionStorage())
->setAuthCodeStorage(new AuthCodeStorage())
->setRefreshTokenStorage(new RefreshTokenStorage())
->setScopeDelimiter(',');
$authServer->setAccessTokenStorage(new AccessTokenStorage());
$authServer->setClientStorage(new ClientStorage());
$authServer->setScopeStorage(new ScopeStorage());
$authServer->setSessionStorage(new SessionStorage());
$authServer->setAuthCodeStorage(new AuthCodeStorage());
$authServer->setRefreshTokenStorage(new RefreshTokenStorage());
$authServer->setScopeDelimiter(',');
$this->_authServer = $authServer;
foreach ($this->grantTypes as $grantType) {
if (!array_key_exists($grantType, $this->grantMap)) {
if (!isset($this->grantMap[$grantType])) {
throw new InvalidConfigException('Invalid grant type');
}
/** @var Grant\GrantTypeInterface $grant */
$grant = new $this->grantMap[$grantType]();
$this->_authServer->addGrantType($grant);
}

View File

@ -0,0 +1,44 @@
<?php
namespace api\components\OAuth2\Entities;
use api\components\OAuth2\Storage\SessionStorage;
use ErrorException;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
protected $sessionId;
public function getSessionId() {
return $this->sessionId;
}
public function setSessionId($sessionId) {
$this->sessionId = $sessionId;
}
/**
* @inheritdoc
* @return static
*/
public function setSession(OriginalSessionEntity $session) {
parent::setSession($session);
$this->sessionId = $session->getId();
return $this;
}
public function getSession() {
if ($this->session instanceof OriginalSessionEntity) {
return $this->session;
}
$sessionStorage = $this->server->getSessionStorage();
if (!$sessionStorage instanceof SessionStorage) {
throw new ErrorException('SessionStorage must be instance of ' . SessionStorage::class);
}
return $sessionStorage->getById($this->sessionId);
}
}

View File

@ -1,11 +1,9 @@
<?php
namespace common\components\oauth\Entity;
namespace api\components\OAuth2\Entities;
use League\OAuth2\Server\Entity\EntityTrait;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
use EntityTrait;
class AuthCodeEntity extends \League\OAuth2\Server\Entity\AuthCodeEntity {
protected $sessionId;
@ -24,4 +22,8 @@ class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
return $this;
}
public function setSessionId(string $sessionId) {
$this->sessionId = $sessionId;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace api\components\OAuth2\Entities;
class ClientEntity extends \League\OAuth2\Server\Entity\ClientEntity {
public function setId(string $id) {
$this->id = $id;
}
public function setName(string $name) {
$this->name = $name;
}
public function setSecret(string $secret) {
$this->secret = $secret;
}
public function setRedirectUri($redirectUri) {
$this->redirectUri = $redirectUri;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace api\components\OAuth2\Entities;
use api\components\OAuth2\Storage\SessionStorage;
use ErrorException;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
class RefreshTokenEntity extends \League\OAuth2\Server\Entity\RefreshTokenEntity {
private $sessionId;
public function isExpired() : bool {
return false;
}
public function getSession() : SessionEntity {
if ($this->session instanceof SessionEntity) {
return $this->session;
}
$sessionStorage = $this->server->getSessionStorage();
if (!$sessionStorage instanceof SessionStorage) {
throw new ErrorException('SessionStorage must be instance of ' . SessionStorage::class);
}
return $sessionStorage->getById($this->sessionId);
}
public function getSessionId() : int {
return $this->sessionId;
}
public function setSession(OriginalSessionEntity $session) {
parent::setSession($session);
$this->setSessionId($session->getId());
return $this;
}
public function setSessionId(int $sessionId) {
$this->sessionId = $sessionId;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace api\components\OAuth2\Entities;
class ScopeEntity extends \League\OAuth2\Server\Entity\ScopeEntity {
public function setId(string $id) {
$this->id = $id;
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace common\components\oauth\Entity;
namespace api\components\OAuth2\Entities;
use League\OAuth2\Server\Entity\ClientEntity;
use League\OAuth2\Server\Entity\ClientEntity as OriginalClientEntity;
use League\OAuth2\Server\Entity\EntityTrait;
class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
@ -13,15 +13,15 @@ class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
return $this->clientId;
}
/**
* @inheritdoc
* @return static
*/
public function associateClient(ClientEntity $client) {
public function associateClient(OriginalClientEntity $client) {
parent::associateClient($client);
$this->clientId = $client->getId();
return $this;
}
public function setClientId(string $clientId) {
$this->clientId = $clientId;
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace common\components\oauth\Exception;
namespace api\components\OAuth2\Exception;
use League\OAuth2\Server\Exception\OAuthException;

View File

@ -1,5 +1,5 @@
<?php
namespace common\components\oauth\Exception;
namespace api\components\OAuth2\Exception;
class AccessDeniedException extends \League\OAuth2\Server\Exception\AccessDeniedException {

View File

@ -0,0 +1,20 @@
<?php
namespace api\components\OAuth2\Grants;
use api\components\OAuth2\Entities;
class AuthCodeGrant extends \League\OAuth2\Server\Grant\AuthCodeGrant {
protected function createAccessTokenEntity() {
return new Entities\AccessTokenEntity($this->server);
}
protected function createRefreshTokenEntity() {
return new Entities\RefreshTokenEntity($this->server);
}
protected function createSessionEntity() {
return new Entities\SessionEntity($this->server);
}
}

View File

@ -0,0 +1,150 @@
<?php
namespace api\components\OAuth2\Grants;
use api\components\OAuth2\Entities;
use ErrorException;
use League\OAuth2\Server\Entity\ClientEntity as OriginalClientEntity;
use League\OAuth2\Server\Entity\RefreshTokenEntity as OriginalRefreshTokenEntity;
use League\OAuth2\Server\Event;
use League\OAuth2\Server\Exception;
use League\OAuth2\Server\Util\SecureKey;
class RefreshTokenGrant extends \League\OAuth2\Server\Grant\RefreshTokenGrant {
public $refreshTokenRotate = false;
protected function createAccessTokenEntity() {
return new Entities\AccessTokenEntity($this->server);
}
protected function createRefreshTokenEntity() {
return new Entities\RefreshTokenEntity($this->server);
}
protected function createSessionEntity() {
return new Entities\SessionEntity($this->server);
}
/**
* Метод таки пришлось переписать по той причине, что нынче мы храним access_token в redis с expire значением,
* так что он может банально несуществовать на тот момент, когда к нему через refresh_token попытаются обратиться.
* Поэтому мы расширили логику RefreshTokenEntity и она теперь знает о сессии, в рамках которой была создана
*
* @inheritdoc
*/
public function completeFlow() {
$clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
if (is_null($clientId)) {
throw new Exception\InvalidRequestException('client_id');
}
$clientSecret = $this->server->getRequest()->request->get(
'client_secret',
$this->server->getRequest()->getPassword()
);
if ($this->shouldRequireClientSecret() && is_null($clientSecret)) {
throw new Exception\InvalidRequestException('client_secret');
}
// Validate client ID and client secret
$client = $this->server->getClientStorage()->get(
$clientId,
$clientSecret,
null,
$this->getIdentifier()
);
if (($client instanceof OriginalClientEntity) === false) {
$this->server->getEventEmitter()->emit(new Event\ClientAuthenticationFailedEvent($this->server->getRequest()));
throw new Exception\InvalidClientException();
}
$oldRefreshTokenParam = $this->server->getRequest()->request->get('refresh_token', null);
if ($oldRefreshTokenParam === null) {
throw new Exception\InvalidRequestException('refresh_token');
}
// Validate refresh token
$oldRefreshToken = $this->server->getRefreshTokenStorage()->get($oldRefreshTokenParam);
if (($oldRefreshToken instanceof OriginalRefreshTokenEntity) === false) {
throw new Exception\InvalidRefreshException();
}
// Ensure the old refresh token hasn't expired
if ($oldRefreshToken->isExpired()) {
throw new Exception\InvalidRefreshException();
}
/** @var Entities\AccessTokenEntity|null $oldAccessToken */
$oldAccessToken = $oldRefreshToken->getAccessToken();
if ($oldAccessToken instanceof Entities\AccessTokenEntity) {
// Get the scopes for the original session
$session = $oldAccessToken->getSession();
} else {
if (!$oldRefreshToken instanceof Entities\RefreshTokenEntity) {
throw new ErrorException('oldRefreshToken must be instance of ' . Entities\RefreshTokenEntity::class);
}
$session = $oldRefreshToken->getSession();
}
$scopes = $this->formatScopes($session->getScopes());
// Get and validate any requested scopes
$requestedScopesString = $this->server->getRequest()->request->get('scope', '');
$requestedScopes = $this->validateScopes($requestedScopesString, $client);
// If no new scopes are requested then give the access token the original session scopes
if (count($requestedScopes) === 0) {
$newScopes = $scopes;
} else {
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
// the request doesn't include any new scopes
foreach ($requestedScopes as $requestedScope) {
if (!isset($scopes[$requestedScope->getId()])) {
throw new Exception\InvalidScopeException($requestedScope->getId());
}
}
$newScopes = $requestedScopes;
}
// Generate a new access token and assign it the correct sessions
$newAccessToken = $this->createAccessTokenEntity();
$newAccessToken->setId(SecureKey::generate());
$newAccessToken->setExpireTime($this->getAccessTokenTTL() + time());
$newAccessToken->setSession($session);
foreach ($newScopes as $newScope) {
$newAccessToken->associateScope($newScope);
}
// Expire the old token and save the new one
($oldAccessToken instanceof Entities\AccessTokenEntity) && $oldAccessToken->expire();
$newAccessToken->save();
$this->server->getTokenType()->setSession($session);
$this->server->getTokenType()->setParam('access_token', $newAccessToken->getId());
$this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
if ($this->shouldRotateRefreshTokens()) {
// Expire the old refresh token
$oldRefreshToken->expire();
// Generate a new refresh token
$newRefreshToken = $this->createRefreshTokenEntity();
$newRefreshToken->setId(SecureKey::generate());
$newRefreshToken->setExpireTime($this->getRefreshTokenTTL() + time());
$newRefreshToken->setAccessToken($newAccessToken);
$newRefreshToken->save();
$this->server->getTokenType()->setParam('refresh_token', $newRefreshToken->getId());
} else {
$oldRefreshToken->setAccessToken($newAccessToken);
$oldRefreshToken->save();
}
return $this->server->getTokenType()->generateResponse();
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace api\components\OAuth2\Storage;
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());
$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');
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace api\components\OAuth2\Storage;
use api\components\OAuth2\Entities\AuthCodeEntity;
use common\components\Redis\Key;
use common\components\Redis\Set;
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AuthCodeInterface;
use yii\helpers\Json;
class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface {
public $dataTable = 'oauth_auth_codes';
public function get($code) {
$result = Json::decode((new Key($this->dataTable, $code))->getValue());
if ($result === null) {
return null;
}
$entity = new AuthCodeEntity($this->server);
$entity->setId($result['id']);
$entity->setExpireTime($result['expire_time']);
$entity->setSessionId($result['session_id']);
$entity->setRedirectUri($result['client_redirect_uri']);
return $entity;
}
public function create($token, $expireTime, $sessionId, $redirectUri) {
$payload = Json::encode([
'id' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
'client_redirect_uri' => $redirectUri,
]);
$this->key($token)->setValue($payload)->expireAt($expireTime);
}
public function getScopes(OriginalAuthCodeEntity $token) {
$scopes = $this->scopes($token->getId());
$scopesEntities = [];
foreach ($scopes as $scope) {
if ($this->server->getScopeStorage()->get($scope) !== null) {
$scopesEntities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
}
return $scopesEntities;
}
public function associateScope(OriginalAuthCodeEntity $token, ScopeEntity $scope) {
$this->scopes($token->getId())->add($scope->getId())->expireAt($token->getExpireTime());
}
public function delete(OriginalAuthCodeEntity $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');
}
}

View File

@ -1,9 +1,9 @@
<?php
namespace common\components\oauth\Storage\Yii2;
namespace api\components\OAuth2\Storage;
use common\components\oauth\Entity\SessionEntity;
use api\components\OAuth2\Entities\ClientEntity;
use api\components\OAuth2\Entities\SessionEntity;
use common\models\OauthClient;
use League\OAuth2\Server\Entity\ClientEntity;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\ClientInterface;
@ -18,15 +18,13 @@ class ClientStorage extends AbstractStorage implements ClientInterface {
* @inheritdoc
*/
public function get($clientId, $clientSecret = null, $redirectUri = null, $grantType = null) {
$query = OauthClient::find()
->select(['id', 'name', 'secret', 'redirect_uri'])
->where([OauthClient::tableName() . '.id' => $clientId]);
$query = OauthClient::find()->andWhere(['id' => $clientId]);
if ($clientSecret !== null) {
$query->andWhere(['secret' => $clientSecret]);
}
$model = $query->asArray()->one();
/** @var OauthClient|null $model */
$model = $query->one();
if ($model === null) {
return null;
}
@ -39,22 +37,17 @@ class ClientStorage extends AbstractStorage implements ClientInterface {
* Короче это нужно учесть
*/
if ($redirectUri !== null) {
if ($redirectUri === self::REDIRECT_STATIC_PAGE || $redirectUri === self::REDIRECT_STATIC_PAGE_WITH_CODE) {
if (in_array($redirectUri, [self::REDIRECT_STATIC_PAGE, self::REDIRECT_STATIC_PAGE_WITH_CODE], true)) {
// Тут, наверное, нужно проверить тип приложения
} else {
if (!StringHelper::startsWith($redirectUri, $model['redirect_uri'], false)) {
if (!StringHelper::startsWith($redirectUri, $model->redirect_uri, false)) {
return null;
}
}
}
$entity = new ClientEntity($this->server);
$entity->hydrate([
'id' => $model['id'],
'name' => $model['name'],
'secret' => $model['secret'],
'redirectUri' => $redirectUri,
]);
$entity = $this->hydrate($model);
$entity->setRedirectUri($redirectUri);
return $entity;
}
@ -67,17 +60,23 @@ class ClientStorage extends AbstractStorage implements ClientInterface {
throw new \ErrorException('This module assumes that $session typeof ' . SessionEntity::class);
}
$model = OauthClient::find()
->select(['id', 'name'])
->andWhere(['id' => $session->getClientId()])
->asArray()
->one();
/** @var OauthClient|null $model */
$model = OauthClient::findOne($session->getClientId());
if ($model === null) {
return null;
}
return (new ClientEntity($this->server))->hydrate($model);
return $this->hydrate($model);
}
private function hydrate(OauthClient $model) : ClientEntity {
$entity = new ClientEntity($this->server);
$entity->setId($model->id);
$entity->setName($model->name);
$entity->setSecret($model->secret);
$entity->setRedirectUri($model->redirect_uri);
return $entity;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace api\components\OAuth2\Storage;
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());
$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);
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace common\components\oauth\Storage\Yii2;
namespace api\components\OAuth2\Storage;
use api\components\OAuth2\Entities\ScopeEntity;
use common\models\OauthScope;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\ScopeInterface;
@ -12,13 +12,12 @@ class ScopeStorage extends AbstractStorage implements ScopeInterface {
* @inheritdoc
*/
public function get($scope, $grantType = null, $clientId = null) {
$row = OauthScope::find()->andWhere(['id' => $scope])->asArray()->one();
if ($row === null) {
if (!in_array($scope, OauthScope::getScopes(), true)) {
return null;
}
$entity = new ScopeEntity($this->server);
$entity->hydrate($row);
$entity->setId($scope);
return $entity;
}

View File

@ -1,8 +1,8 @@
<?php
namespace common\components\oauth\Storage\Yii2;
namespace api\components\OAuth2\Storage;
use common\components\oauth\Entity\AuthCodeEntity;
use common\components\oauth\Entity\SessionEntity;
use api\components\OAuth2\Entities\AuthCodeEntity;
use api\components\OAuth2\Entities\SessionEntity;
use common\models\OauthSession;
use ErrorException;
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
@ -11,85 +11,41 @@ use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\SessionInterface;
use yii\db\ActiveQuery;
use yii\db\Exception;
class SessionStorage extends AbstractStorage implements SessionInterface {
private $cache = [];
/**
* @param string $sessionId
* @return OauthSession|null
*/
private function getSessionModel($sessionId) {
if (!isset($this->cache[$sessionId])) {
$this->cache[$sessionId] = OauthSession::findOne($sessionId);
}
return $this->cache[$sessionId];
}
private function hydrateEntity($sessionModel) {
if (!$sessionModel instanceof OauthSession) {
return null;
}
return (new SessionEntity($this->server))->hydrate([
'id' => $sessionModel->id,
'client_id' => $sessionModel->client_id,
])->setOwner($sessionModel->owner_type, $sessionModel->owner_id);
}
/**
* @param string $sessionId
* @return SessionEntity|null
*/
public function getSession($sessionId) {
return $this->hydrateEntity($this->getSessionModel($sessionId));
public function getById($sessionId) {
return $this->hydrate($this->getSessionModel($sessionId));
}
/**
* @inheritdoc
*/
public function getByAccessToken(OriginalAccessTokenEntity $accessToken) {
/** @var OauthSession|null $model */
$model = OauthSession::find()->innerJoinWith([
'accessTokens' => function(ActiveQuery $query) use ($accessToken) {
$query->andWhere(['access_token' => $accessToken->getId()]);
},
])->one();
return $this->hydrateEntity($model);
throw new ErrorException('This method is not implemented and should not be used');
}
/**
* @inheritdoc
*/
public function getByAuthCode(OriginalAuthCodeEntity $authCode) {
if (!$authCode instanceof AuthCodeEntity) {
throw new ErrorException('This module assumes that $authCode typeof ' . AuthCodeEntity::class);
}
return $this->getSession($authCode->getSessionId());
return $this->getById($authCode->getSessionId());
}
/**
* {@inheritdoc}
*/
public function getScopes(OriginalSessionEntity $session) {
$result = [];
foreach ($this->getSessionModel($session->getId())->getScopes() as $scope) {
// TODO: нужно проверить все выданные скоупы на их существование
if ($this->server->getScopeStorage()->get($scope) !== null) {
$result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
}
return $result;
}
/**
* @inheritdoc
*/
public function create($ownerType, $ownerId, $clientId, $clientRedirectUri = null) {
$sessionId = OauthSession::find()
->select('id')
@ -116,11 +72,26 @@ class SessionStorage extends AbstractStorage implements SessionInterface {
return $sessionId;
}
/**
* @inheritdoc
*/
public function associateScope(OriginalSessionEntity $session, ScopeEntity $scope) {
$this->getSessionModel($session->getId())->getScopes()->add($scope->getId());
}
private function getSessionModel(string $sessionId) : OauthSession {
$session = OauthSession::findOne($sessionId);
if ($session === null) {
throw new ErrorException('Cannot find oauth session');
}
return $session;
}
private function hydrate(OauthSession $sessionModel) {
$entity = new SessionEntity($this->server);
$entity->setId($sessionModel->id);
$entity->setClientId($sessionModel->client_id);
$entity->setOwner($sessionModel->owner_type, $sessionModel->owner_id);
return $entity;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace api\components\OAuth2\Utils\KeyAlgorithm;
use League\OAuth2\Server\Util\KeyAlgorithm\KeyAlgorithmInterface;
use Ramsey\Uuid\Uuid;
class UuidAlgorithm implements KeyAlgorithmInterface {
/**
* @inheritdoc
*/
public function generate($len = 40) : string {
return Uuid::uuid4()->toString();
}
}

View File

@ -1,7 +1,7 @@
<?php
$params = array_merge(
require(__DIR__ . '/../../common/config/params.php'),
require(__DIR__ . '/params.php')
require __DIR__ . '/../../common/config/params.php',
require __DIR__ . '/params.php'
);
return [
@ -63,8 +63,12 @@ return [
'format' => yii\web\Response::FORMAT_JSON,
],
'oauth' => [
'class' => common\components\oauth\Component::class,
'class' => api\components\OAuth2\Component::class,
'grantTypes' => ['authorization_code'],
'grantMap' => [
'authorization_code' => api\components\OAuth2\Grants\AuthCodeGrant::class,
'refresh_token' => api\components\OAuth2\Grants\RefreshTokenGrant::class,
],
],
'errorHandler' => [
'class' => api\components\ErrorHandler::class,

View File

@ -2,13 +2,12 @@
namespace api\controllers;
use api\filters\ActiveUserRule;
use common\components\oauth\Exception\AcceptRequiredException;
use common\components\oauth\Exception\AccessDeniedException;
use api\components\OAuth2\Exception\AcceptRequiredException;
use api\components\OAuth2\Exception\AccessDeniedException;
use common\models\Account;
use common\models\OauthClient;
use common\models\OauthScope;
use League\OAuth2\Server\Exception\OAuthException;
use League\OAuth2\Server\Grant\RefreshTokenGrant;
use Yii;
use yii\filters\AccessControl;
use yii\helpers\ArrayHelper;
@ -186,7 +185,7 @@ class OauthController extends Controller {
}
$scopes = $codeModel->getScopes();
if (array_search(OauthScope::OFFLINE_ACCESS, array_keys($scopes)) === false) {
if (array_search(OauthScope::OFFLINE_ACCESS, array_keys($scopes), true) === false) {
return;
}
} elseif ($grantType === 'refresh_token') {
@ -195,7 +194,10 @@ class OauthController extends Controller {
return;
}
$this->getServer()->addGrantType(new RefreshTokenGrant());
$grantClass = Yii::$app->oauth->grantMap['refresh_token'];
$grant = new $grantClass;
$this->getServer()->addGrantType($grant);
}
/**

View File

@ -25,6 +25,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
/**
* @inheritdoc
* @throws TooManyRequestsHttpException
*/
public function beforeAction($action) {
$this->checkRateLimit(
@ -39,6 +40,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
/**
* @inheritdoc
* @throws TooManyRequestsHttpException
*/
public function checkRateLimit($user, $request, $response, $action) {
if (parse_url($request->getHostInfo(), PHP_URL_HOST) === $this->authserverDomain) {
@ -54,7 +56,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
$key = $this->buildKey($ip);
$redis = $this->getRedis();
$countRequests = intval($redis->executeCommand('INCR', [$key]));
$countRequests = (int)$redis->incr($key);
if ($countRequests === 1) {
$redis->executeCommand('EXPIRE', [$key, $this->limitTime]);
}
@ -65,7 +67,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
}
/**
* @return \yii\redis\Connection
* @return \common\components\Redis\Connection
*/
public function getRedis() {
return Yii::$app->redis;

View File

@ -128,7 +128,7 @@ class JoinForm extends Model {
$account = $accessModel->account;
}
/** @var MinecraftAccessKey|\common\models\OauthAccessToken $accessModel */
/** @var MinecraftAccessKey|\api\components\OAuth2\Entities\AccessTokenEntity $accessModel */
if ($accessModel->isExpired()) {
Session::error("User with access_token = '{$accessToken}' failed join by expired access_token.");
throw new ForbiddenOperationException('Expired access_token.');

View File

@ -17,7 +17,7 @@ class Yii extends \yii\BaseYii {
* Used for properties that are identical for both WebApplication and ConsoleApplication
*
* @property \yii\swiftmailer\Mailer $mailer
* @property \yii\redis\Connection $redis
* @property \common\components\Redis\Connection $redis
* @property \common\components\RabbitMQ\Component $amqp
* @property \GuzzleHttp\Client $guzzle
* @property \common\components\EmailRenderer $emailRenderer
@ -32,7 +32,7 @@ abstract class BaseApplication extends yii\base\Application {
* @property \api\components\User\Component $user User component.
* @property \api\components\ApiUser\Component $apiUser Api User component.
* @property \api\components\ReCaptcha\Component $reCaptcha
* @property \common\components\oauth\Component $oauth
* @property \api\components\OAuth2\Component $oauth
*
* @method \api\components\User\Component getUser()
*/

View File

@ -0,0 +1,13 @@
<?php
namespace common\components\Redis;
use yii\di\Instance;
class Cache extends \yii\redis\Cache {
public function init() {
\yii\caching\Cache::init();
$this->redis = Instance::ensure($this->redis, ConnectionInterface::class);
}
}

View File

@ -0,0 +1,415 @@
<?php
namespace common\components\Redis;
use Predis\Client;
use Predis\ClientInterface;
use yii\base\Component;
/**
* Interface defining a client able to execute commands against Redis.
*
* All the commands exposed by the client generally have the same signature as
* described by the Redis documentation, but some of them offer an additional
* and more friendly interface to ease programming which is described in the
* following list of methods:
*
* @method int del(array $keys)
* @method string dump($key)
* @method int exists($key)
* @method int expire($key, $seconds)
* @method int expireat($key, $timestamp)
* @method array keys($pattern)
* @method int move($key, $db)
* @method mixed object($subcommand, $key)
* @method int persist($key)
* @method int pexpire($key, $milliseconds)
* @method int pexpireat($key, $timestamp)
* @method int pttl($key)
* @method string randomkey()
* @method mixed rename($key, $target)
* @method int renamenx($key, $target)
* @method array scan($cursor, array $options = null)
* @method array sort($key, array $options = null)
* @method int ttl($key)
* @method mixed type($key)
* @method int append($key, $value)
* @method int bitcount($key, $start = null, $end = null)
* @method int bitop($operation, $destkey, $key)
* @method int decr($key)
* @method int decrby($key, $decrement)
* @method string get($key)
* @method int getbit($key, $offset)
* @method string getrange($key, $start, $end)
* @method string getset($key, $value)
* @method int incr($key)
* @method int incrby($key, $increment)
* @method string incrbyfloat($key, $increment)
* @method array mget(array $keys)
* @method mixed mset(array $dictionary)
* @method int msetnx(array $dictionary)
* @method mixed psetex($key, $milliseconds, $value)
* @method mixed set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
* @method int setbit($key, $offset, $value)
* @method int setex($key, $seconds, $value)
* @method int setnx($key, $value)
* @method int setrange($key, $offset, $value)
* @method int strlen($key)
* @method int hdel($key, array $fields)
* @method int hexists($key, $field)
* @method string hget($key, $field)
* @method array hgetall($key)
* @method int hincrby($key, $field, $increment)
* @method string hincrbyfloat($key, $field, $increment)
* @method array hkeys($key)
* @method int hlen($key)
* @method array hmget($key, array $fields)
* @method mixed hmset($key, array $dictionary)
* @method array hscan($key, $cursor, array $options = null)
* @method int hset($key, $field, $value)
* @method int hsetnx($key, $field, $value)
* @method array hvals($key)
* @method array blpop(array $keys, $timeout)
* @method array brpop(array $keys, $timeout)
* @method array brpoplpush($source, $destination, $timeout)
* @method string lindex($key, $index)
* @method int linsert($key, $whence, $pivot, $value)
* @method int llen($key)
* @method string lpop($key)
* @method int lpush($key, array $values)
* @method int lpushx($key, $value)
* @method array lrange($key, $start, $stop)
* @method int lrem($key, $count, $value)
* @method mixed lset($key, $index, $value)
* @method mixed ltrim($key, $start, $stop)
* @method string rpop($key)
* @method string rpoplpush($source, $destination)
* @method int rpush($key, array $values)
* @method int rpushx($key, $value)
* @method int sadd($key, array $members)
* @method int scard($key)
* @method array sdiff(array $keys)
* @method int sdiffstore($destination, array $keys)
* @method array sinter(array $keys)
* @method int sinterstore($destination, array $keys)
* @method int sismember($key, $member)
* @method array smembers($key)
* @method int smove($source, $destination, $member)
* @method string spop($key)
* @method string srandmember($key, $count = null)
* @method int srem($key, $member)
* @method array sscan($key, $cursor, array $options = null)
* @method array sunion(array $keys)
* @method int sunionstore($destination, array $keys)
* @method int zadd($key, array $membersAndScoresDictionary)
* @method int zcard($key)
* @method string zcount($key, $min, $max)
* @method string zincrby($key, $increment, $member)
* @method int zinterstore($destination, array $keys, array $options = null)
* @method array zrange($key, $start, $stop, array $options = null)
* @method array zrangebyscore($key, $min, $max, array $options = null)
* @method int zrank($key, $member)
* @method int zrem($key, $member)
* @method int zremrangebyrank($key, $start, $stop)
* @method int zremrangebyscore($key, $min, $max)
* @method array zrevrange($key, $start, $stop, array $options = null)
* @method array zrevrangebyscore($key, $min, $max, array $options = null)
* @method int zrevrank($key, $member)
* @method int zunionstore($destination, array $keys, array $options = null)
* @method string zscore($key, $member)
* @method array zscan($key, $cursor, array $options = null)
* @method array zrangebylex($key, $start, $stop, array $options = null)
* @method int zremrangebylex($key, $min, $max)
* @method int zlexcount($key, $min, $max)
* @method int pfadd($key, array $elements)
* @method mixed pfmerge($destinationKey, array $sourceKeys)
* @method int pfcount(array $keys)
* @method mixed pubsub($subcommand, $argument)
* @method int publish($channel, $message)
* @method mixed discard()
* @method array exec()
* @method mixed multi()
* @method mixed unwatch()
* @method mixed watch($key)
* @method mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method mixed evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method mixed script($subcommand, $argument = null)
* @method mixed auth($password)
* @method string echo($message)
* @method mixed ping($message = null)
* @method mixed select($database)
* @method mixed bgrewriteaof()
* @method mixed bgsave()
* @method mixed client($subcommand, $argument = null)
* @method mixed config($subcommand, $argument = null)
* @method int dbsize()
* @method mixed flushall()
* @method mixed flushdb()
* @method array info($section = null)
* @method int lastsave()
* @method mixed save()
* @method mixed slaveof($host, $port)
* @method mixed slowlog($subcommand, $argument = null)
* @method array time()
* @method array command()
*/
class Connection extends Component implements ConnectionInterface {
/**
* @var array List of available redis commands http://redis.io/commands
*/
const REDIS_COMMANDS = [
'BLPOP', // key [key ...] timeout Remove and get the first element in a list, or block until one is available
'BRPOP', // key [key ...] timeout Remove and get the last element in a list, or block until one is available
'BRPOPLPUSH', // source destination timeout Pop a value from a list, push it to another list and return it; or block until one is available
'CLIENT KILL', // ip:port Kill the connection of a client
'CLIENT LIST', // Get the list of client connections
'CLIENT GETNAME', // Get the current connection name
'CLIENT SETNAME', // connection-name Set the current connection name
'CONFIG GET', // parameter Get the value of a configuration parameter
'CONFIG SET', // parameter value Set a configuration parameter to the given value
'CONFIG RESETSTAT', // Reset the stats returned by INFO
'DBSIZE', // Return the number of keys in the selected database
'DEBUG OBJECT', // key Get debugging information about a key
'DEBUG SEGFAULT', // Make the server crash
'DECR', // key Decrement the integer value of a key by one
'DECRBY', // key decrement Decrement the integer value of a key by the given number
'DEL', // key [key ...] Delete a key
'DISCARD', // Discard all commands issued after MULTI
'DUMP', // key Return a serialized version of the value stored at the specified key.
'ECHO', // message Echo the given string
'EVAL', // script numkeys key [key ...] arg [arg ...] Execute a Lua script server side
'EVALSHA', // sha1 numkeys key [key ...] arg [arg ...] Execute a Lua script server side
'EXEC', // Execute all commands issued after MULTI
'EXISTS', // key Determine if a key exists
'EXPIRE', // key seconds Set a key's time to live in seconds
'EXPIREAT', // key timestamp Set the expiration for a key as a UNIX timestamp
'FLUSHALL', // Remove all keys from all databases
'FLUSHDB', // Remove all keys from the current database
'GET', // key Get the value of a key
'GETBIT', // key offset Returns the bit value at offset in the string value stored at key
'GETRANGE', // key start end Get a substring of the string stored at a key
'GETSET', // key value Set the string value of a key and return its old value
'HDEL', // key field [field ...] Delete one or more hash fields
'HEXISTS', // key field Determine if a hash field exists
'HGET', // key field Get the value of a hash field
'HGETALL', // key Get all the fields and values in a hash
'HINCRBY', // key field increment Increment the integer value of a hash field by the given number
'HINCRBYFLOAT', // key field increment Increment the float value of a hash field by the given amount
'HKEYS', // key Get all the fields in a hash
'HLEN', // key Get the number of fields in a hash
'HMGET', // key field [field ...] Get the values of all the given hash fields
'HMSET', // key field value [field value ...] Set multiple hash fields to multiple values
'HSET', // key field value Set the string value of a hash field
'HSETNX', // key field value Set the value of a hash field, only if the field does not exist
'HVALS', // key Get all the values in a hash
'INCR', // key Increment the integer value of a key by one
'INCRBY', // key increment Increment the integer value of a key by the given amount
'INCRBYFLOAT', // key increment Increment the float value of a key by the given amount
'INFO', // [section] Get information and statistics about the server
'KEYS', // pattern Find all keys matching the given pattern
'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk
'LINDEX', // key index Get an element from a list by its index
'LINSERT', // key BEFORE|AFTER pivot value Insert an element before or after another element in a list
'LLEN', // key Get the length of a list
'LPOP', // key Remove and get the first element in a list
'LPUSH', // key value [value ...] Prepend one or multiple values to a list
'LPUSHX', // key value Prepend a value to a list, only if the list exists
'LRANGE', // key start stop Get a range of elements from a list
'LREM', // key count value Remove elements from a list
'LSET', // key index value Set the value of an element in a list by its index
'LTRIM', // key start stop Trim a list to the specified range
'MGET', // key [key ...] Get the values of all the given keys
'MIGRATE', // host port key destination-db timeout Atomically transfer a key from a Redis instance to another one.
'MONITOR', // Listen for all requests received by the server in real time
'MOVE', // key db Move a key to another database
'MSET', // key value [key value ...] Set multiple keys to multiple values
'MSETNX', // key value [key value ...] Set multiple keys to multiple values, only if none of the keys exist
'MULTI', // Mark the start of a transaction block
'OBJECT', // subcommand [arguments [arguments ...]] Inspect the internals of Redis objects
'PERSIST', // key Remove the expiration from a key
'PEXPIRE', // key milliseconds Set a key's time to live in milliseconds
'PEXPIREAT', // key milliseconds-timestamp Set the expiration for a key as a UNIX timestamp specified in milliseconds
'PING', // Ping the server
'PSETEX', // key milliseconds value Set the value and expiration in milliseconds of a key
'PSUBSCRIBE', // pattern [pattern ...] Listen for messages published to channels matching the given patterns
'PTTL', // key Get the time to live for a key in milliseconds
'PUBLISH', // channel message Post a message to a channel
'PUNSUBSCRIBE', // [pattern [pattern ...]] Stop listening for messages posted to channels matching the given patterns
'QUIT', // Close the connection
'RANDOMKEY', // Return a random key from the keyspace
'RENAME', // key newkey Rename a key
'RENAMENX', // key newkey Rename a key, only if the new key does not exist
'RESTORE', // key ttl serialized-value Create a key using the provided serialized value, previously obtained using DUMP.
'RPOP', // key Remove and get the last element in a list
'RPOPLPUSH', // source destination Remove the last element in a list, append it to another list and return it
'RPUSH', // key value [value ...] Append one or multiple values to a list
'RPUSHX', // key value Append a value to a list, only if the list exists
'SADD', // key member [member ...] Add one or more members to a set
'SAVE', // Synchronously save the dataset to disk
'SCARD', // key Get the number of members in a set
'SCRIPT EXISTS', // script [script ...] Check existence of scripts in the script cache.
'SCRIPT FLUSH', // Remove all the scripts from the script cache.
'SCRIPT KILL', // Kill the script currently in execution.
'SCRIPT LOAD', // script Load the specified Lua script into the script cache.
'SDIFF', // key [key ...] Subtract multiple sets
'SDIFFSTORE', // destination key [key ...] Subtract multiple sets and store the resulting set in a key
'SELECT', // index Change the selected database for the current connection
'SET', // key value Set the string value of a key
'SETBIT', // key offset value Sets or clears the bit at offset in the string value stored at key
'SETEX', // key seconds value Set the value and expiration of a key
'SETNX', // key value Set the value of a key, only if the key does not exist
'SETRANGE', // key offset value Overwrite part of a string at key starting at the specified offset
'SHUTDOWN', // [NOSAVE] [SAVE] Synchronously save the dataset to disk and then shut down the server
'SINTER', // key [key ...] Intersect multiple sets
'SINTERSTORE', // destination key [key ...] Intersect multiple sets and store the resulting set in a key
'SISMEMBER', // key member Determine if a given value is a member of a set
'SLAVEOF', // host port Make the server a slave of another instance, or promote it as master
'SLOWLOG', // subcommand [argument] Manages the Redis slow queries log
'SMEMBERS', // key Get all the members in a set
'SMOVE', // source destination member Move a member from one set to another
'SORT', // key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] Sort the elements in a list, set or sorted set
'SPOP', // key Remove and return a random member from a set
'SRANDMEMBER', // key [count] Get one or multiple random members from a set
'SREM', // key member [member ...] Remove one or more members from a set
'STRLEN', // key Get the length of the value stored in a key
'SUBSCRIBE', // channel [channel ...] Listen for messages published to the given channels
'SUNION', // key [key ...] Add multiple sets
'SUNIONSTORE', // destination key [key ...] Add multiple sets and store the resulting set in a key
'SYNC', // Internal command used for replication
'TIME', // Return the current server time
'TTL', // key Get the time to live for a key
'TYPE', // key Determine the type stored at key
'UNSUBSCRIBE', // [channel [channel ...]] Stop listening for messages posted to the given channels
'UNWATCH', // Forget about all watched keys
'WATCH', // key [key ...] Watch the given keys to determine execution of the MULTI/EXEC block
'ZADD', // key score member [score member ...] Add one or more members to a sorted set, or update its score if it already exists
'ZCARD', // key Get the number of members in a sorted set
'ZCOUNT', // key min max Count the members in a sorted set with scores within the given values
'ZINCRBY', // key increment member Increment the score of a member in a sorted set
'ZINTERSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Intersect multiple sorted sets and store the resulting sorted set in a new key
'ZRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index
'ZRANGEBYSCORE', // key min max [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score
'ZRANK', // key member Determine the index of a member in a sorted set
'ZREM', // key member [member ...] Remove one or more members from a sorted set
'ZREMRANGEBYRANK', // key start stop Remove all members in a sorted set within the given indexes
'ZREMRANGEBYSCORE', // key min max Remove all members in a sorted set within the given scores
'ZREVRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index, with scores ordered from high to low
'ZREVRANGEBYSCORE', // key max min [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score, with scores ordered from high to low
'ZREVRANK', // key member Determine the index of a member in a sorted set, with scores ordered from high to low
'ZSCORE', // key member Get the score associated with the given member in a sorted set
'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key
'GEOADD', // key longitude latitude member [longitude latitude member ...] Add point
'GEODIST', // key member1 member2 [unit] Return the distance between two members
'GEOHASH', // key member [member ...] Return valid Geohash strings
'GEOPOS', // key member [member ...] Return the positions (longitude,latitude)
'GEORADIUS', // key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] Return the members
'GEORADIUSBYMEMBER', // key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
];
/**
* @var string the hostname or ip address to use for connecting to the redis server. Defaults to 'localhost'.
* If [[unixSocket]] is specified, hostname and port will be ignored.
*/
public $hostname = 'localhost';
/**
* @var integer the port to use for connecting to the redis server. Default port is 6379.
* If [[unixSocket]] is specified, hostname and port will be ignored.
*/
public $port = 6379;
/**
* @var string the unix socket path (e.g. `/var/run/redis/redis.sock`) to use for connecting to the redis server.
* This can be used instead of [[hostname]] and [[port]] to connect to the server using a unix socket.
* If a unix socket path is specified, [[hostname]] and [[port]] will be ignored.
*/
public $unixSocket;
/**
* @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send.
* See http://redis.io/commands/auth
*/
public $password;
/**
* @var integer the redis database to use. This is an integer value starting from 0. Defaults to 0.
*/
public $database = 0;
/**
* @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout")
*/
public $connectionTimeout;
/**
* @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used.
*/
public $dataTimeout;
/**
* @var integer Bitmask field which may be set to any combination of connection flags passed to [stream_socket_client()](http://php.net/manual/en/function.stream-socket-client.php).
* Currently the select of connection flags is limited to `STREAM_CLIENT_CONNECT` (default), `STREAM_CLIENT_ASYNC_CONNECT` and `STREAM_CLIENT_PERSISTENT`.
* @see http://php.net/manual/en/function.stream-socket-client.php
*/
public $socketClientFlags = STREAM_CLIENT_CONNECT;
/**
* @var array|null
*/
public $parameters;
/**
* @var array
*/
public $options = [];
/**
* @var ClientInterface
*/
private $_client;
public function getConnection() : ClientInterface {
if ($this->_client === null) {
$this->_client = new Client($this->prepareParams(), $this->options);
}
return $this->_client;
}
public function __call($name, $params) {
$redisCommand = mb_strtoupper($name);
if (in_array($redisCommand, self::REDIS_COMMANDS)) {
return $this->executeCommand($name, $params);
}
return parent::__call($name, $params);
}
public function executeCommand(string $name, array $params = []) {
return $this->getConnection()->$name(...$params);
}
private function prepareParams() {
if ($this->parameters !== null) {
return $this->parameters;
}
if ($this->unixSocket) {
$parameters = [
'scheme' => 'unix',
'path' => $this->unixSocket,
];
} else {
$parameters = [
'scheme' => 'tcp',
'host' => $this->hostname,
'port' => $this->port,
];
}
return array_merge($parameters, [
'database' => $this->database,
]);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace common\components\Redis;
interface ConnectionInterface {
/**
* @return ConnectionInterface
*/
public function getConnection();
/**
* @param string $name Command, that should be executed
* @param array $params Arguments for this command
*
* @return mixed
*/
public function executeCommand(string $name, array $params = []);
}

View File

@ -1,5 +1,5 @@
<?php
namespace common\components\redis;
namespace common\components\Redis;
use InvalidArgumentException;
use Yii;
@ -9,35 +9,52 @@ class Key {
protected $key;
/**
* @return \yii\redis\Connection
* @return Connection
*/
public function getRedis() {
return Yii::$app->redis;
}
public function getKey() {
public function getKey() : string {
return $this->key;
}
public function getValue() {
return json_decode($this->getRedis()->get($this->key), true);
return $this->getRedis()->get($this->key);
}
public function setValue($value) {
$this->getRedis()->set($this->key, json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$this->getRedis()->set($this->key, $value);
return $this;
}
public function delete() {
$this->getRedis()->executeCommand('DEL', [$this->key]);
$this->getRedis()->del($this->key);
return $this;
}
public function expire($ttl) {
$this->getRedis()->executeCommand('EXPIRE', [$this->key, $ttl]);
public function exists() : bool {
return (bool)$this->getRedis()->exists($this->key);
}
public function expire(int $ttl) {
$this->getRedis()->expire($this->key, $ttl);
return $this;
}
public function expireAt(int $unixTimestamp) {
$this->getRedis()->expireat($this->key, $unixTimestamp);
return $this;
}
public function __construct(...$key) {
if (empty($key)) {
throw new InvalidArgumentException('You must specify at least one key.');
}
$this->key = $this->buildKey($key);
}
private function buildKey(array $parts) {
$keyParts = [];
foreach($parts as $part) {
@ -47,12 +64,4 @@ class Key {
return implode(':', $keyParts);
}
public function __construct(...$key) {
if (empty($key)) {
throw new InvalidArgumentException('You must specify at least one key.');
}
$this->key = $this->buildKey($key);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace common\components\Redis;
use ArrayIterator;
use IteratorAggregate;
class Set extends Key implements IteratorAggregate {
public function add($value) {
$this->getRedis()->sadd($this->key, $value);
return $this;
}
public function remove($value) {
$this->getRedis()->srem($this->key, $value);
return $this;
}
public function members() {
return $this->getRedis()->smembers($this->key);
}
public function getValue() {
return $this->members();
}
public function exists(string $value = null) : bool {
if ($value === null) {
return parent::exists();
} else {
return (bool)$this->getRedis()->sismember($this->key, $value);
}
}
public function diff(array $sets) {
return $this->getRedis()->sdiff([$this->key, implode(' ', $sets)]);
}
/**
* @inheritdoc
*/
public function getIterator() {
return new ArrayIterator($this->members());
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace common\components\oauth\Entity;
use League\OAuth2\Server\Entity\EntityTrait;
use League\OAuth2\Server\Entity\SessionEntity;
class AuthCodeEntity extends \League\OAuth2\Server\Entity\AuthCodeEntity {
use EntityTrait;
protected $sessionId;
public function getSessionId() {
return $this->sessionId;
}
/**
* @inheritdoc
* @return static
*/
public function setSession(SessionEntity $session) {
parent::setSession($session);
$this->sessionId = $session->getId();
return $this;
}
}

View File

@ -1,84 +0,0 @@
<?php
namespace common\components\oauth\Storage\Redis;
use common\components\oauth\Entity\AuthCodeEntity;
use common\components\redis\Key;
use common\components\redis\Set;
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AuthCodeInterface;
class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface {
public $dataTable = 'oauth_auth_codes';
public $ttl = 3600; // 1h
/**
* @inheritdoc
*/
public function get($code) {
$result = (new Key($this->dataTable, $code))->getValue();
if (!$result) {
return null;
}
if ($result['expire_time'] < time()) {
return null;
}
return (new AuthCodeEntity($this->server))->hydrate([
'id' => $result['id'],
'redirectUri' => $result['client_redirect_uri'],
'expireTime' => $result['expire_time'],
'sessionId' => $result['session_id'],
]);
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $sessionId, $redirectUri) {
$payload = [
'id' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
'client_redirect_uri' => $redirectUri,
];
(new Key($this->dataTable, $token))->setValue($payload)->expire($this->ttl);
}
/**
* @inheritdoc
*/
public function getScopes(OriginalAuthCodeEntity $token) {
$result = new Set($this->dataTable, $token->getId(), 'scopes');
$response = [];
foreach ($result as $scope) {
// TODO: нужно проверить все выданные скоупы на их существование
$response[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $response;
}
/**
* @inheritdoc
*/
public function associateScope(OriginalAuthCodeEntity $token, ScopeEntity $scope) {
(new Set($this->dataTable, $token->getId(), 'scopes'))->add($scope->getId())->expire($this->ttl);
}
/**
* @inheritdoc
*/
public function delete(OriginalAuthCodeEntity $token) {
// Удаляем ключ
(new Set($this->dataTable, $token->getId()))->delete();
// Удаляем список скоупов для ключа
(new Set($this->dataTable, $token->getId(), 'scopes'))->delete();
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace common\components\oauth\Storage\Redis;
use common\components\redis\Key;
use League\OAuth2\Server\Entity\RefreshTokenEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\RefreshTokenInterface;
class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterface {
public $dataTable = 'oauth_refresh_tokens';
/**
* @inheritdoc
*/
public function get($token) {
$result = (new Key($this->dataTable, $token))->getValue();
if (!$result) {
return null;
}
return (new RefreshTokenEntity($this->server))
->setId($result['id'])
->setExpireTime($result['expire_time'])
->setAccessTokenId($result['access_token_id']);
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $accessToken) {
$payload = [
'id' => $token,
'expire_time' => $expireTime,
'access_token_id' => $accessToken,
];
(new Key($this->dataTable, $token))->setValue($payload);
}
/**
* @inheritdoc
*/
public function delete(RefreshTokenEntity $token) {
(new Key($this->dataTable, $token->getId()))->delete();
}
}

View File

@ -1,84 +0,0 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\components\oauth\Entity\AccessTokenEntity;
use common\models\OauthAccessToken;
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\db\Exception;
class AccessTokenStorage extends AbstractStorage implements AccessTokenInterface {
private $cache = [];
/**
* @param string $token
* @return OauthAccessToken|null
*/
private function getTokenModel($token) {
if (!isset($this->cache[$token])) {
$this->cache[$token] = OauthAccessToken::findOne($token);
}
return $this->cache[$token];
}
/**
* @inheritdoc
*/
public function get($token) {
$model = $this->getTokenModel($token);
if ($model === null) {
return null;
}
return (new AccessTokenEntity($this->server))->hydrate([
'id' => $model->access_token,
'expireTime' => $model->expire_time,
'sessionId' => $model->session_id,
]);
}
/**
* @inheritdoc
*/
public function getScopes(OriginalAccessTokenEntity $token) {
$entities = [];
foreach($this->getTokenModel($token->getId())->getScopes() as $scope) {
$entities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $entities;
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $sessionId) {
$model = new OauthAccessToken();
$model->access_token = $token;
$model->expire_time = $expireTime;
$model->session_id = $sessionId;
if (!$model->save()) {
throw new Exception('Cannot save ' . OauthAccessToken::class . ' model.');
}
}
/**
* @inheritdoc
*/
public function associateScope(OriginalAccessTokenEntity $token, ScopeEntity $scope) {
$this->getTokenModel($token->getId())->getScopes()->add($scope->getId());
}
/**
* @inheritdoc
*/
public function delete(OriginalAccessTokenEntity $token) {
$this->getTokenModel($token->getId())->delete();
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace common\components\oauth\Util\KeyAlgorithm;
use League\OAuth2\Server\Util\KeyAlgorithm\DefaultAlgorithm;
use Ramsey\Uuid\Uuid;
class UuidAlgorithm extends DefaultAlgorithm {
/**
* @inheritdoc
*/
public function generate($len = 40) : string {
return Uuid::uuid4()->toString();
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace common\components\redis;
use IteratorAggregate;
use Yii;
class Set extends Key implements IteratorAggregate {
/**
* @return \yii\redis\Connection
*/
public static function getDb() {
return Yii::$app->redis;
}
public function add($value) {
$this->getDb()->executeCommand('SADD', [$this->key, $value]);
return $this;
}
public function remove($value) {
$this->getDb()->executeCommand('SREM', [$this->key, $value]);
return $this;
}
public function members() {
return $this->getDb()->executeCommand('SMEMBERS', [$this->key]);
}
public function getValue() {
return $this->members();
}
public function exists($value) {
return !!$this->getDb()->executeCommand('SISMEMBER', [$this->key, $value]);
}
public function diff(array $sets) {
return $this->getDb()->executeCommand('SDIFF', [$this->key, implode(' ', $sets)]);
}
/**
* @inheritdoc
*/
public function getIterator() {
return new \ArrayIterator($this->members());
}
}

View File

@ -3,7 +3,7 @@ return [
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'components' => [
'cache' => [
'class' => yii\redis\Cache::class,
'class' => common\components\Redis\Cache::class,
'redis' => 'redis',
],
'db' => [
@ -24,7 +24,7 @@ return [
'passwordHashStrategy' => 'password_hash',
],
'redis' => [
'class' => yii\redis\Connection::class,
'class' => common\components\Redis\Connection::class,
'hostname' => 'redis',
'password' => null,
'port' => 6379,

View File

@ -1,7 +1,7 @@
<?php
namespace common\models;
use common\components\redis\Set;
use common\components\Redis\Set;
use yii\db\ActiveRecord;
/**
@ -15,6 +15,7 @@ use yii\db\ActiveRecord;
*
* Отношения:
* @property OauthSession $session
* @deprecated
*/
class OauthAccessToken extends ActiveRecord {

View File

@ -1,21 +1,20 @@
<?php
namespace common\models;
use yii\db\ActiveRecord;
/**
* Поля:
* @property string $id
*/
class OauthScope extends ActiveRecord {
class OauthScope {
const OFFLINE_ACCESS = 'offline_access';
const MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
const ACCOUNT_INFO = 'account_info';
const ACCOUNT_EMAIL = 'account_email';
public static function tableName() {
return '{{%oauth_scopes}}';
public static function getScopes() : array {
return [
self::OFFLINE_ACCESS,
self::MINECRAFT_SERVER_SESSION,
self::ACCOUNT_INFO,
self::ACCOUNT_EMAIL,
];
}
}

View File

@ -1,7 +1,9 @@
<?php
namespace common\models;
use common\components\redis\Set;
use common\components\Redis\Set;
use Yii;
use yii\base\ErrorException;
use yii\db\ActiveRecord;
/**
@ -13,7 +15,6 @@ use yii\db\ActiveRecord;
* @property string $client_redirect_uri
*
* Отношения
* @property OauthAccessToken[] $accessTokens
* @property OauthClient $client
* @property Account $account
* @property Set $scopes
@ -25,7 +26,7 @@ class OauthSession extends ActiveRecord {
}
public function getAccessTokens() {
return $this->hasMany(OauthAccessToken::class, ['session_id' => 'id']);
throw new ErrorException('This method is possible, but not implemented');
}
public function getClient() {
@ -46,6 +47,14 @@ class OauthSession extends ActiveRecord {
}
$this->getScopes()->delete();
/** @var \api\components\OAuth2\Storage\RefreshTokenStorage $refreshTokensStorage */
$refreshTokensStorage = Yii::$app->oauth->getAuthServer()->getRefreshTokenStorage();
$refreshTokensSet = $refreshTokensStorage->sessionHash($this->id);
foreach ($refreshTokensSet->members() as $refreshTokenId) {
$refreshTokensStorage->delete($refreshTokensStorage->get($refreshTokenId));
}
$refreshTokensSet->delete();
return true;
}

View File

@ -18,14 +18,15 @@
"yiisoft/yii2": "2.0.9",
"yiisoft/yii2-swiftmailer": "*",
"ramsey/uuid": "^3.5.0",
"league/oauth2-server": "~4.1.5",
"league/oauth2-server": "dev-improvements#b9277ccd664dcb80a766b73674d21de686cb9dda",
"yiisoft/yii2-redis": "~2.0.0",
"guzzlehttp/guzzle": "^6.0.0",
"php-amqplib/php-amqplib": "^2.6.2",
"ely/yii2-tempmail-validator": "~1.0.0",
"emarref/jwt": "~1.0.3",
"ely/amqp-controller": "dev-master#d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec",
"ely/email-renderer": "dev-master#38a148cd5081147acc31125ddc49966b149f65cf"
"ely/email-renderer": "dev-master#38a148cd5081147acc31125ddc49966b149f65cf",
"predis/predis": "^1.0"
},
"require-dev": {
"yiisoft/yii2-codeception": "*",
@ -35,8 +36,7 @@
"codeception/codeception": "~2.2.4",
"codeception/specify": "*",
"codeception/verify": "*",
"phploc/phploc": "^3.0.1",
"predis/predis": "^1.0"
"phploc/phploc": "^3.0.1"
},
"config": {
"process-timeout": 1800
@ -53,6 +53,10 @@
{
"type": "git",
"url": "git@gitlab.com:elyby/email-renderer.git"
},
{
"type": "git",
"url": "git@gitlab.ely.by:elyby/oauth2-server.git"
}
],
"scripts": {

View File

@ -0,0 +1,25 @@
<?php
use console\db\Migration;
class m161127_145211_remove_oauth_scopes extends Migration {
public function safeUp() {
$this->dropTable('{{%oauth_scopes}}');
}
public function safeDown() {
$this->createTable('{{%oauth_scopes}}', [
'id' => $this->string(64),
$this->primary('id'),
]);
$this->batchInsert('{{%oauth_scopes}}', ['id'], [
['offline_access'],
['minecraft_server_session'],
['account_info'],
['account_email'],
]);
}
}

View File

@ -23,14 +23,7 @@ class OauthRefreshTokenCest {
'ely',
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM'
));
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'token_type' => 'Bearer',
]);
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
$I->canSeeResponseJsonMatchesJsonPath('$.refresh_token');
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
$this->canSeeRefreshTokenSuccess($I);
}
public function testRefreshTokenWithSameScopes(OauthSteps $I) {
@ -41,14 +34,26 @@ class OauthRefreshTokenCest {
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
[S::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
));
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'token_type' => 'Bearer',
]);
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
$I->canSeeResponseJsonMatchesJsonPath('$.refresh_token');
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
$this->canSeeRefreshTokenSuccess($I);
}
public function testRefreshTokenTwice(OauthSteps $I) {
$refreshToken = $I->getRefreshToken([S::MINECRAFT_SERVER_SESSION]);
$this->route->issueToken($this->buildParams(
$refreshToken,
'ely',
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
[S::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
));
$this->canSeeRefreshTokenSuccess($I);
$this->route->issueToken($this->buildParams(
$refreshToken,
'ely',
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
[S::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
));
$this->canSeeRefreshTokenSuccess($I);
}
public function testRefreshTokenWithNewScopes(OauthSteps $I) {
@ -91,4 +96,15 @@ class OauthRefreshTokenCest {
return $params;
}
private function canSeeRefreshTokenSuccess(OauthSteps $I) {
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'token_type' => 'Bearer',
]);
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
$I->cantSeeResponseJsonMatchesJsonPath('$.refresh_token');
}
}

View File

@ -1,7 +1,6 @@
<?php
namespace tests\codeception\common\fixtures;
use common\models\OauthScope;
use common\models\OauthSession;
use yii\test\ActiveFixture;