diff --git a/Dockerfile b/Dockerfile index 24a74dc..7877162 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 эта папка будет скопирована обратно. diff --git a/Dockerfile-dev b/Dockerfile-dev index 67b7943..7c30fc4 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -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 эта папка будет скопирована обратно. diff --git a/api/components/ApiUser/AuthChecker.php b/api/components/ApiUser/AuthChecker.php index ef3dae5..59f6fa9 100644 --- a/api/components/ApiUser/AuthChecker.php +++ b/api/components/ApiUser/AuthChecker.php @@ -1,7 +1,7 @@ oauth->getAuthServer()->getAccessTokenStorage()->get($token); if ($accessToken === null) { return false; } - return $accessToken->getScopes()->exists($permissionName); + return $accessToken->hasScope($permissionName); } } diff --git a/api/components/ApiUser/Identity.php b/api/components/ApiUser/Identity.php index 3000b93..fb3510d 100644 --- a/api/components/ApiUser/Identity.php +++ b/api/components/ApiUser/Identity.php @@ -1,24 +1,25 @@ 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() { diff --git a/common/components/oauth/Component.php b/api/components/OAuth2/Component.php similarity index 57% rename from common/components/oauth/Component.php rename to api/components/OAuth2/Component.php index 6bd3091..940e29b 100644 --- a/common/components/oauth/Component.php +++ b/api/components/OAuth2/Component.php @@ -1,13 +1,13 @@ _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); } diff --git a/api/components/OAuth2/Entities/AccessTokenEntity.php b/api/components/OAuth2/Entities/AccessTokenEntity.php new file mode 100644 index 0000000..183704d --- /dev/null +++ b/api/components/OAuth2/Entities/AccessTokenEntity.php @@ -0,0 +1,44 @@ +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); + } + +} diff --git a/common/components/oauth/Entity/AccessTokenEntity.php b/api/components/OAuth2/Entities/AuthCodeEntity.php similarity index 66% rename from common/components/oauth/Entity/AccessTokenEntity.php rename to api/components/OAuth2/Entities/AuthCodeEntity.php index bd70930..28bfc2b 100644 --- a/common/components/oauth/Entity/AccessTokenEntity.php +++ b/api/components/OAuth2/Entities/AuthCodeEntity.php @@ -1,11 +1,9 @@ sessionId = $sessionId; + } + } diff --git a/api/components/OAuth2/Entities/ClientEntity.php b/api/components/OAuth2/Entities/ClientEntity.php new file mode 100644 index 0000000..8636cf1 --- /dev/null +++ b/api/components/OAuth2/Entities/ClientEntity.php @@ -0,0 +1,22 @@ +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; + } + +} diff --git a/api/components/OAuth2/Entities/RefreshTokenEntity.php b/api/components/OAuth2/Entities/RefreshTokenEntity.php new file mode 100644 index 0000000..2404fa7 --- /dev/null +++ b/api/components/OAuth2/Entities/RefreshTokenEntity.php @@ -0,0 +1,44 @@ +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; + } + +} diff --git a/api/components/OAuth2/Entities/ScopeEntity.php b/api/components/OAuth2/Entities/ScopeEntity.php new file mode 100644 index 0000000..7b9f3c0 --- /dev/null +++ b/api/components/OAuth2/Entities/ScopeEntity.php @@ -0,0 +1,10 @@ +id = $id; + } + +} diff --git a/common/components/oauth/Entity/SessionEntity.php b/api/components/OAuth2/Entities/SessionEntity.php similarity index 57% rename from common/components/oauth/Entity/SessionEntity.php rename to api/components/OAuth2/Entities/SessionEntity.php index 28fafb5..eea6fb3 100644 --- a/common/components/oauth/Entity/SessionEntity.php +++ b/api/components/OAuth2/Entities/SessionEntity.php @@ -1,7 +1,7 @@ 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; + } + } diff --git a/common/components/oauth/Exception/AcceptRequiredException.php b/api/components/OAuth2/Exception/AcceptRequiredException.php similarity index 89% rename from common/components/oauth/Exception/AcceptRequiredException.php rename to api/components/OAuth2/Exception/AcceptRequiredException.php index 36c5bf0..540650c 100644 --- a/common/components/oauth/Exception/AcceptRequiredException.php +++ b/api/components/OAuth2/Exception/AcceptRequiredException.php @@ -1,5 +1,5 @@ server); + } + + protected function createRefreshTokenEntity() { + return new Entities\RefreshTokenEntity($this->server); + } + + protected function createSessionEntity() { + return new Entities\SessionEntity($this->server); + } + +} diff --git a/api/components/OAuth2/Grants/RefreshTokenGrant.php b/api/components/OAuth2/Grants/RefreshTokenGrant.php new file mode 100644 index 0000000..d98b3d6 --- /dev/null +++ b/api/components/OAuth2/Grants/RefreshTokenGrant.php @@ -0,0 +1,150 @@ +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(); + } + +} diff --git a/api/components/OAuth2/Storage/AccessTokenStorage.php b/api/components/OAuth2/Storage/AccessTokenStorage.php new file mode 100644 index 0000000..fdeb14c --- /dev/null +++ b/api/components/OAuth2/Storage/AccessTokenStorage.php @@ -0,0 +1,67 @@ +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'); + } + +} diff --git a/api/components/OAuth2/Storage/AuthCodeStorage.php b/api/components/OAuth2/Storage/AuthCodeStorage.php new file mode 100644 index 0000000..77d7f51 --- /dev/null +++ b/api/components/OAuth2/Storage/AuthCodeStorage.php @@ -0,0 +1,72 @@ +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'); + } + +} diff --git a/common/components/oauth/Storage/Yii2/ClientStorage.php b/api/components/OAuth2/Storage/ClientStorage.php similarity index 67% rename from common/components/oauth/Storage/Yii2/ClientStorage.php rename to api/components/OAuth2/Storage/ClientStorage.php index 5e8808d..90d024b 100644 --- a/common/components/oauth/Storage/Yii2/ClientStorage.php +++ b/api/components/OAuth2/Storage/ClientStorage.php @@ -1,9 +1,9 @@ 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; } } diff --git a/api/components/OAuth2/Storage/RefreshTokenStorage.php b/api/components/OAuth2/Storage/RefreshTokenStorage.php new file mode 100644 index 0000000..2321e76 --- /dev/null +++ b/api/components/OAuth2/Storage/RefreshTokenStorage.php @@ -0,0 +1,60 @@ +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); + } + +} diff --git a/common/components/oauth/Storage/Yii2/ScopeStorage.php b/api/components/OAuth2/Storage/ScopeStorage.php similarity index 64% rename from common/components/oauth/Storage/Yii2/ScopeStorage.php rename to api/components/OAuth2/Storage/ScopeStorage.php index 64fef0e..be42d1e 100644 --- a/common/components/oauth/Storage/Yii2/ScopeStorage.php +++ b/api/components/OAuth2/Storage/ScopeStorage.php @@ -1,8 +1,8 @@ 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; } diff --git a/common/components/oauth/Storage/Yii2/SessionStorage.php b/api/components/OAuth2/Storage/SessionStorage.php similarity index 55% rename from common/components/oauth/Storage/Yii2/SessionStorage.php rename to api/components/OAuth2/Storage/SessionStorage.php index 1542391..8777f96 100644 --- a/common/components/oauth/Storage/Yii2/SessionStorage.php +++ b/api/components/OAuth2/Storage/SessionStorage.php @@ -1,8 +1,8 @@ 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: нужно проверить все выданные скоупы на их существование - $result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]); + 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; + } + } diff --git a/api/components/OAuth2/Utils/KeyAlgorithm/UuidAlgorithm.php b/api/components/OAuth2/Utils/KeyAlgorithm/UuidAlgorithm.php new file mode 100644 index 0000000..54b4ba3 --- /dev/null +++ b/api/components/OAuth2/Utils/KeyAlgorithm/UuidAlgorithm.php @@ -0,0 +1,16 @@ +toString(); + } + +} diff --git a/api/config/config.php b/api/config/config.php index dfcd9c3..4e7ab1a 100644 --- a/api/config/config.php +++ b/api/config/config.php @@ -1,7 +1,7 @@ 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, diff --git a/api/controllers/OauthController.php b/api/controllers/OauthController.php index 4f5db86..5c19972 100644 --- a/api/controllers/OauthController.php +++ b/api/controllers/OauthController.php @@ -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); } /** diff --git a/api/modules/session/filters/RateLimiter.php b/api/modules/session/filters/RateLimiter.php index 71cbf65..12cd5b7 100644 --- a/api/modules/session/filters/RateLimiter.php +++ b/api/modules/session/filters/RateLimiter.php @@ -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; diff --git a/api/modules/session/models/JoinForm.php b/api/modules/session/models/JoinForm.php index 648338e..f5e1973 100644 --- a/api/modules/session/models/JoinForm.php +++ b/api/modules/session/models/JoinForm.php @@ -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.'); diff --git a/autocompletion.php b/autocompletion.php index 24c5140..1d99674 100644 --- a/autocompletion.php +++ b/autocompletion.php @@ -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 @@ -29,10 +29,10 @@ abstract class BaseApplication extends yii\base\Application { * Class WebApplication * Include only Web application related components here * - * @property \api\components\User\Component $user User component. - * @property \api\components\ApiUser\Component $apiUser Api User component. + * @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() */ diff --git a/common/components/Redis/Cache.php b/common/components/Redis/Cache.php new file mode 100644 index 0000000..6a120a1 --- /dev/null +++ b/common/components/Redis/Cache.php @@ -0,0 +1,13 @@ +redis = Instance::ensure($this->redis, ConnectionInterface::class); + } + +} diff --git a/common/components/Redis/Connection.php b/common/components/Redis/Connection.php new file mode 100644 index 0000000..5bdc47d --- /dev/null +++ b/common/components/Redis/Connection.php @@ -0,0 +1,415 @@ +_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, + ]); + } + +} diff --git a/common/components/Redis/ConnectionInterface.php b/common/components/Redis/ConnectionInterface.php new file mode 100644 index 0000000..f4195fe --- /dev/null +++ b/common/components/Redis/ConnectionInterface.php @@ -0,0 +1,19 @@ +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); - } - } diff --git a/common/components/Redis/Set.php b/common/components/Redis/Set.php new file mode 100644 index 0000000..fe8302a --- /dev/null +++ b/common/components/Redis/Set.php @@ -0,0 +1,46 @@ +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()); + } + +} diff --git a/common/components/oauth/Entity/AuthCodeEntity.php b/common/components/oauth/Entity/AuthCodeEntity.php deleted file mode 100644 index 6bd8b0c..0000000 --- a/common/components/oauth/Entity/AuthCodeEntity.php +++ /dev/null @@ -1,27 +0,0 @@ -sessionId; - } - - /** - * @inheritdoc - * @return static - */ - public function setSession(SessionEntity $session) { - parent::setSession($session); - $this->sessionId = $session->getId(); - - return $this; - } - -} diff --git a/common/components/oauth/Storage/Redis/AuthCodeStorage.php b/common/components/oauth/Storage/Redis/AuthCodeStorage.php deleted file mode 100644 index f3bdbdc..0000000 --- a/common/components/oauth/Storage/Redis/AuthCodeStorage.php +++ /dev/null @@ -1,84 +0,0 @@ -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(); - } - -} diff --git a/common/components/oauth/Storage/Redis/RefreshTokenStorage.php b/common/components/oauth/Storage/Redis/RefreshTokenStorage.php deleted file mode 100644 index f3ad9e0..0000000 --- a/common/components/oauth/Storage/Redis/RefreshTokenStorage.php +++ /dev/null @@ -1,48 +0,0 @@ -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(); - } - -} diff --git a/common/components/oauth/Storage/Yii2/AccessTokenStorage.php b/common/components/oauth/Storage/Yii2/AccessTokenStorage.php deleted file mode 100644 index 03cc89a..0000000 --- a/common/components/oauth/Storage/Yii2/AccessTokenStorage.php +++ /dev/null @@ -1,84 +0,0 @@ -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(); - } - -} diff --git a/common/components/oauth/Util/KeyAlgorithm/UuidAlgorithm.php b/common/components/oauth/Util/KeyAlgorithm/UuidAlgorithm.php deleted file mode 100644 index e75580d..0000000 --- a/common/components/oauth/Util/KeyAlgorithm/UuidAlgorithm.php +++ /dev/null @@ -1,16 +0,0 @@ -toString(); - } - -} diff --git a/common/components/redis/Set.php b/common/components/redis/Set.php deleted file mode 100644 index 80f8d0a..0000000 --- a/common/components/redis/Set.php +++ /dev/null @@ -1,49 +0,0 @@ -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()); - } - -} diff --git a/common/config/config.php b/common/config/config.php index 31b0a83..262b2ec 100644 --- a/common/config/config.php +++ b/common/config/config.php @@ -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, diff --git a/common/models/OauthAccessToken.php b/common/models/OauthAccessToken.php index b253ef2..7364a79 100644 --- a/common/models/OauthAccessToken.php +++ b/common/models/OauthAccessToken.php @@ -1,7 +1,7 @@ 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; } diff --git a/composer.json b/composer.json index fb6029c..ae78d32 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/console/migrations/m161127_145211_remove_oauth_scopes.php b/console/migrations/m161127_145211_remove_oauth_scopes.php new file mode 100644 index 0000000..39b89ce --- /dev/null +++ b/console/migrations/m161127_145211_remove_oauth_scopes.php @@ -0,0 +1,25 @@ +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'], + ]); + } + +} diff --git a/tests/codeception/api/functional/OauthRefreshTokenCest.php b/tests/codeception/api/functional/OauthRefreshTokenCest.php index dc6307a..47e5e47 100644 --- a/tests/codeception/api/functional/OauthRefreshTokenCest.php +++ b/tests/codeception/api/functional/OauthRefreshTokenCest.php @@ -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'); + } + } diff --git a/tests/codeception/common/fixtures/OauthSessionFixture.php b/tests/codeception/common/fixtures/OauthSessionFixture.php index 0bbab87..fbeb903 100644 --- a/tests/codeception/common/fixtures/OauthSessionFixture.php +++ b/tests/codeception/common/fixtures/OauthSessionFixture.php @@ -1,7 +1,6 @@