From 0b63dc2d84abc4dcee62ba7c01eade8b8713dee8 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 23 Aug 2019 11:28:04 +0300 Subject: [PATCH] Upgrade oauth2-server to 8.0.0 version, rewrite repositories and entities, start rewriting tests. Intermediate commit [skip ci] --- api/components/OAuth2/Component.php | 52 +++--- .../OAuth2/Entities/AccessTokenEntity.php | 2 +- .../OAuth2/Entities/AuthCodeEntity.php | 33 ++-- .../OAuth2/Entities/ClientEntity.php | 33 ++-- .../OAuth2/Entities/RefreshTokenEntity.php | 43 +---- .../OAuth2/Entities/ScopeEntity.php | 14 +- .../Exception/AcceptRequiredException.php | 22 --- .../Exception/AccessDeniedException.php | 11 -- .../OAuth2/Grants/AuthCodeGrant.php | 2 +- api/components/OAuth2/Keys/EmptyKey.php | 18 ++ .../Repositories/AccessTokenRepository.php | 56 ++++++ .../AccessTokenStorage.php | 2 +- .../Repositories/AuthCodeRepository.php | 26 +++ .../AuthCodeStorage.php | 2 +- .../OAuth2/Repositories/ClientRepository.php | 41 +++++ .../ClientStorage.php | 2 +- .../Repositories/RefreshTokenRepository.php | 51 ++++++ .../RefreshTokenStorage.php | 2 +- .../OAuth2/Repositories/ScopeRepository.php | 37 ++++ .../ScopeStorage.php | 2 +- .../SessionStorage.php | 2 +- api/components/User/OAuth2Identity.php | 1 + .../controllers/AuthorizationController.php | 7 +- api/modules/oauth/models/OauthProcess.php | 153 ++++++++-------- api/tests/_pages/OauthRoute.php | 32 ++++ api/tests/functional/_steps/OauthSteps.php | 14 +- api/tests/functional/oauth/AuthCodeCest.php | 57 +----- .../functional/oauth/RefreshTokenCest.php | 2 +- api/tests/functional/oauth/ValidateCest.php | 62 +++++++ common/models/OauthClient.php | 4 +- common/models/OauthSession.php | 3 +- composer.json | 8 +- composer.lock | 171 +++++++++++------- 33 files changed, 604 insertions(+), 363 deletions(-) delete mode 100644 api/components/OAuth2/Exception/AcceptRequiredException.php delete mode 100644 api/components/OAuth2/Exception/AccessDeniedException.php create mode 100644 api/components/OAuth2/Keys/EmptyKey.php create mode 100644 api/components/OAuth2/Repositories/AccessTokenRepository.php rename api/components/OAuth2/{Storage => Repositories}/AccessTokenStorage.php (97%) create mode 100644 api/components/OAuth2/Repositories/AuthCodeRepository.php rename api/components/OAuth2/{Storage => Repositories}/AuthCodeStorage.php (98%) create mode 100644 api/components/OAuth2/Repositories/ClientRepository.php rename api/components/OAuth2/{Storage => Repositories}/ClientStorage.php (98%) create mode 100644 api/components/OAuth2/Repositories/RefreshTokenRepository.php rename api/components/OAuth2/{Storage => Repositories}/RefreshTokenStorage.php (97%) create mode 100644 api/components/OAuth2/Repositories/ScopeRepository.php rename api/components/OAuth2/{Storage => Repositories}/ScopeStorage.php (98%) rename api/components/OAuth2/{Storage => Repositories}/SessionStorage.php (98%) create mode 100644 api/tests/functional/oauth/ValidateCest.php diff --git a/api/components/OAuth2/Component.php b/api/components/OAuth2/Component.php index 0fa3e4f..c09f939 100644 --- a/api/components/OAuth2/Component.php +++ b/api/components/OAuth2/Component.php @@ -1,10 +1,13 @@ _authServer === null) { - $authServer = new AuthorizationServer(); - $authServer->setAccessTokenStorage(new Storage\AccessTokenStorage()); - $authServer->setClientStorage(new Storage\ClientStorage()); - $authServer->setScopeStorage(new Storage\ScopeStorage()); - $authServer->setSessionStorage(new Storage\SessionStorage()); - $authServer->setAuthCodeStorage(new Storage\AuthCodeStorage()); - $authServer->setRefreshTokenStorage(new Storage\RefreshTokenStorage()); - $authServer->setAccessTokenTTL(86400); // 1d + $clientsRepo = new Repositories\ClientRepository(); + $accessTokensRepo = new Repositories\AccessTokenRepository(); + $scopesRepo = new Repositories\ScopeRepository(); + $authCodesRepo = new Repositories\AuthCodeRepository(); + $refreshTokensRepo = new Repositories\RefreshTokenRepository(); - $authServer->addGrantType(new Grants\AuthCodeGrant()); - $authServer->addGrantType(new Grants\RefreshTokenGrant()); - $authServer->addGrantType(new Grants\ClientCredentialsGrant()); + $accessTokenTTL = new DateInterval('P1D'); + + $authServer = new AuthorizationServer( + $clientsRepo, + $accessTokensRepo, + $scopesRepo, + new EmptyKey(), + '123' // TODO: extract to the variable + ); + /** @noinspection PhpUnhandledExceptionInspection */ + $authCodeGrant = new Grant\AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M')); + $authCodeGrant->disableRequireCodeChallengeForPublicClients(); + $authServer->enableGrantType($authCodeGrant, $accessTokenTTL); + $authServer->enableGrantType(new Grant\RefreshTokenGrant($refreshTokensRepo), $accessTokenTTL); + $authServer->enableGrantType(new Grant\ClientCredentialsGrant(), $accessTokenTTL); $this->_authServer = $authServer; } @@ -38,16 +50,4 @@ class Component extends BaseComponent { return $this->_authServer; } - public function getAccessTokenStorage(): AccessTokenInterface { - return $this->getAuthServer()->getAccessTokenStorage(); - } - - public function getRefreshTokenStorage(): RefreshTokenInterface { - return $this->getAuthServer()->getRefreshTokenStorage(); - } - - public function getSessionStorage(): SessionInterface { - return $this->getAuthServer()->getSessionStorage(); - } - } diff --git a/api/components/OAuth2/Entities/AccessTokenEntity.php b/api/components/OAuth2/Entities/AccessTokenEntity.php index 183704d..ad876ed 100644 --- a/api/components/OAuth2/Entities/AccessTokenEntity.php +++ b/api/components/OAuth2/Entities/AccessTokenEntity.php @@ -1,7 +1,7 @@ sessionId; - } - - /** - * @inheritdoc - * @return static - */ - public function setSession(OriginalSessionEntity $session) { - parent::setSession($session); - $this->sessionId = $session->getId(); - - return $this; - } - - public function setSessionId(string $sessionId) { - $this->sessionId = $sessionId; - } + // TODO: constructor } diff --git a/api/components/OAuth2/Entities/ClientEntity.php b/api/components/OAuth2/Entities/ClientEntity.php index e88f424..be9f68a 100644 --- a/api/components/OAuth2/Entities/ClientEntity.php +++ b/api/components/OAuth2/Entities/ClientEntity.php @@ -1,32 +1,21 @@ id = $id; - } - - public function setName(string $name) { + public function __construct(string $id, string $name, $redirectUri, bool $isTrusted = false) { + $this->identifier = $id; $this->name = $name; - } - - public function setSecret(string $secret) { - $this->secret = $secret; - } - - public function setRedirectUri($redirectUri) { $this->redirectUri = $redirectUri; - } - - public function setIsTrusted(bool $isTrusted) { - $this->isTrusted = $isTrusted; - } - - public function isTrusted(): bool { - return $this->isTrusted; + $this->isConfidential = $isTrusted; } } diff --git a/api/components/OAuth2/Entities/RefreshTokenEntity.php b/api/components/OAuth2/Entities/RefreshTokenEntity.php index 372f003..aea8f5a 100644 --- a/api/components/OAuth2/Entities/RefreshTokenEntity.php +++ b/api/components/OAuth2/Entities/RefreshTokenEntity.php @@ -3,43 +3,12 @@ declare(strict_types=1); namespace api\components\OAuth2\Entities; -use api\components\OAuth2\Storage\SessionStorage; -use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity; -use Webmozart\Assert\Assert; +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait; -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; - } - - /** @var SessionStorage $sessionStorage */ - $sessionStorage = $this->server->getSessionStorage(); - Assert::isInstanceOf($sessionStorage, SessionStorage::class); - - return $sessionStorage->getById($this->sessionId); - } - - public function getSessionId(): int { - return $this->sessionId; - } - - public function setSession(OriginalSessionEntity $session): self { - parent::setSession($session); - $this->setSessionId((int)$session->getId()); - - return $this; - } - - public function setSessionId(int $sessionId): void { - $this->sessionId = $sessionId; - } +class RefreshTokenEntity implements RefreshTokenEntityInterface { + use EntityTrait; + use RefreshTokenTrait; } diff --git a/api/components/OAuth2/Entities/ScopeEntity.php b/api/components/OAuth2/Entities/ScopeEntity.php index 7b9f3c0..24895c2 100644 --- a/api/components/OAuth2/Entities/ScopeEntity.php +++ b/api/components/OAuth2/Entities/ScopeEntity.php @@ -1,10 +1,18 @@ id = $id; +class ScopeEntity implements ScopeEntityInterface { + use EntityTrait; + use ScopeTrait; + + public function __construct(string $id) { + $this->identifier = $id; } } diff --git a/api/components/OAuth2/Exception/AcceptRequiredException.php b/api/components/OAuth2/Exception/AcceptRequiredException.php deleted file mode 100644 index 540650c..0000000 --- a/api/components/OAuth2/Exception/AcceptRequiredException.php +++ /dev/null @@ -1,22 +0,0 @@ -redirectUri = $redirectUri; - } - -} diff --git a/api/components/OAuth2/Grants/AuthCodeGrant.php b/api/components/OAuth2/Grants/AuthCodeGrant.php index 069dfa2..35f66a0 100644 --- a/api/components/OAuth2/Grants/AuthCodeGrant.php +++ b/api/components/OAuth2/Grants/AuthCodeGrant.php @@ -6,7 +6,7 @@ use api\components\OAuth2\Entities\AuthCodeEntity; use api\components\OAuth2\Entities\ClientEntity; use api\components\OAuth2\Entities\RefreshTokenEntity; use api\components\OAuth2\Entities\SessionEntity; -use api\components\OAuth2\Storage\ScopeStorage; +use api\components\OAuth2\Repositories\ScopeStorage; use api\components\OAuth2\Utils\Scopes; use League\OAuth2\Server\Entity\AuthCodeEntity as BaseAuthCodeEntity; use League\OAuth2\Server\Entity\ClientEntity as BaseClientEntity; diff --git a/api/components/OAuth2/Keys/EmptyKey.php b/api/components/OAuth2/Keys/EmptyKey.php new file mode 100644 index 0000000..daf017e --- /dev/null +++ b/api/components/OAuth2/Keys/EmptyKey.php @@ -0,0 +1,18 @@ +findModel($clientId); + if ($client === null) { + return null; + } + + return new ClientEntity($client->id, $client->name, $client->redirect_uri, (bool)$client->is_trusted); + } + + public function validateClient($clientId, $clientSecret, $grantType): bool { + $client = $this->findModel($clientId); + if ($client === null) { + return false; + } + + if ($clientSecret !== null && $clientSecret !== $client->secret) { + return false; + } + + // TODO: there is missing behavior of checking redirectUri. Is it now bundled into grant? + + return true; + } + + private function findModel(string $id): ?OauthClient { + return OauthClient::findOne(['id' => $id]); + } + +} diff --git a/api/components/OAuth2/Storage/ClientStorage.php b/api/components/OAuth2/Repositories/ClientStorage.php similarity index 98% rename from api/components/OAuth2/Storage/ClientStorage.php rename to api/components/OAuth2/Repositories/ClientStorage.php index fa1aae4..878d97a 100644 --- a/api/components/OAuth2/Storage/ClientStorage.php +++ b/api/components/OAuth2/Repositories/ClientStorage.php @@ -1,5 +1,5 @@ oauth->getAccessTokenStorage()->get($token); if ($model === null) { throw new UnauthorizedHttpException('Incorrect token'); diff --git a/api/modules/oauth/controllers/AuthorizationController.php b/api/modules/oauth/controllers/AuthorizationController.php index 51b1ae4..b98e89b 100644 --- a/api/modules/oauth/controllers/AuthorizationController.php +++ b/api/modules/oauth/controllers/AuthorizationController.php @@ -1,4 +1,6 @@ oauth->authServer; - $server->setRequest(null); // Enforce request recreation (test environment bug) - - return new OauthProcess($server); + return new OauthProcess(Yii::$app->oauth->authServer); } } diff --git a/api/modules/oauth/models/OauthProcess.php b/api/modules/oauth/models/OauthProcess.php index ee0ebd6..d7d6461 100644 --- a/api/modules/oauth/models/OauthProcess.php +++ b/api/modules/oauth/models/OauthProcess.php @@ -1,19 +1,18 @@ getAuthorizationCodeGrant()->checkAuthorizeParams(); - $client = $authParams->getClient(); + $request = $this->getRequest(); + $authRequest = $this->server->validateAuthorizationRequest($request); + $client = $authRequest->getClient(); /** @var OauthClient $clientModel */ - $clientModel = $this->findClient($client->getId()); - $response = $this->buildSuccessResponse( - Yii::$app->request->getQueryParams(), - $clientModel, - $authParams->getScopes() - ); - } catch (OAuthException $e) { + $clientModel = $this->findClient($client->getIdentifier()); + $response = $this->buildSuccessResponse($request, $clientModel, $authRequest->getScopes()); + } catch (OAuthServerException $e) { $response = $this->buildErrorResponse($e); } @@ -88,33 +84,37 @@ class OauthProcess { public function complete(): array { try { Yii::$app->statsd->inc('oauth.complete.attempt'); - $grant = $this->getAuthorizationCodeGrant(); - $authParams = $grant->checkAuthorizeParams(); + + $request = $this->getRequest(); + $authRequest = $this->server->validateAuthorizationRequest($request); /** @var Account $account */ $account = Yii::$app->user->identity->getAccount(); - /** @var \common\models\OauthClient $clientModel */ - $clientModel = $this->findClient($authParams->getClient()->getId()); + /** @var OauthClient $clientModel */ + $clientModel = $this->findClient($authRequest->getClient()->getIdentifier()); - if (!$this->canAutoApprove($account, $clientModel, $authParams)) { + if (!$this->canAutoApprove($account, $clientModel, $authRequest)) { Yii::$app->statsd->inc('oauth.complete.approve_required'); - $isAccept = Yii::$app->request->post('accept'); - if ($isAccept === null) { - throw new AcceptRequiredException(); + + $accept = ((array)$request->getParsedBody())['accept'] ?? null; + if ($accept === null) { + throw $this->createAcceptRequiredException(); } - if (!$isAccept) { - throw new AccessDeniedException($authParams->getRedirectUri()); + if (!in_array($accept, [1, '1', true, 'true'], true)) { + throw OAuthServerException::accessDenied(null, $authRequest->getRedirectUri()); } } - $redirectUri = $grant->newAuthorizeRequest('user', $account->id, $authParams); + $responseObj = $this->server->completeAuthorizationRequest($authRequest, new Response(200)); + $response = [ 'success' => true, - 'redirectUri' => $redirectUri, + 'redirectUri' => $responseObj->getHeader('Location'), // TODO: ensure that this is correct type and behavior ]; + Yii::$app->statsd->inc('oauth.complete.success'); - } catch (OAuthException $e) { - if (!$e instanceof AcceptRequiredException) { + } catch (OAuthServerException $e) { + if ($e->getErrorType() === 'accept_required') { Yii::$app->statsd->inc('oauth.complete.fail'); } @@ -146,19 +146,28 @@ class OauthProcess { * @return array */ public function getToken(): array { - $grantType = Yii::$app->request->post('grant_type', 'null'); + $request = $this->getRequest(); + $params = (array)$request->getParsedBody(); + $grantType = $params['grant_type'] ?? 'null'; try { Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.attempt"); - $response = $this->server->issueAccessToken(); - $clientId = Yii::$app->request->post('client_id'); + + $responseObj = new Response(200); + $this->server->respondToAccessTokenRequest($request, $responseObj); + $clientId = $params['client_id']; + + // TODO: build response from the responseObj + $response = []; + Yii::$app->statsd->inc("oauth.issueToken_client.{$clientId}"); Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.success"); - } catch (OAuthException $e) { + } catch (OAuthServerException $e) { Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.fail"); - Yii::$app->response->statusCode = $e->httpStatusCode; + Yii::$app->response->statusCode = $e->getHttpStatusCode(); + $response = [ - 'error' => $e->errorType, - 'message' => $e->getMessage(), + 'error' => $e->getErrorType(), + 'message' => $e->getMessage(), // TODO: use hint field? ]; } @@ -166,7 +175,7 @@ class OauthProcess { } private function findClient(string $clientId): ?OauthClient { - return OauthClient::findOne($clientId); + return OauthClient::findOne(['id' => $clientId]); } /** @@ -175,11 +184,11 @@ class OauthProcess { * * @param Account $account * @param OauthClient $client - * @param AuthorizeParams $oauthParams + * @param AuthorizationRequest $request * * @return bool */ - private function canAutoApprove(Account $account, OauthClient $client, AuthorizeParams $oauthParams): bool { + private function canAutoApprove(Account $account, OauthClient $client, AuthorizationRequest $request): bool { if ($client->is_trusted) { return true; } @@ -188,7 +197,7 @@ class OauthProcess { $session = $account->getOauthSessions()->andWhere(['client_id' => $client->id])->one(); if ($session !== null) { $existScopes = $session->getScopes()->members(); - if (empty(array_diff(array_keys($oauthParams->getScopes()), $existScopes))) { + if (empty(array_diff(array_keys($request->getScopes()), $existScopes))) { return true; } } @@ -197,17 +206,17 @@ class OauthProcess { } /** - * @param array $queryParams + * @param ServerRequestInterface $request * @param OauthClient $client - * @param \api\components\OAuth2\Entities\ScopeEntity[] $scopes + * @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes * * @return array */ - private function buildSuccessResponse(array $queryParams, OauthClient $client, array $scopes): array { + private function buildSuccessResponse(ServerRequestInterface $request, OauthClient $client, array $scopes): array { return [ 'success' => true, // We return only those keys which are related to the OAuth2 standard parameters - 'oAuth' => array_intersect_key($queryParams, array_flip([ + 'oAuth' => array_intersect_key($request->getQueryParams(), array_flip([ 'client_id', 'redirect_uri', 'response_type', @@ -217,55 +226,57 @@ class OauthProcess { 'client' => [ 'id' => $client->id, 'name' => $client->name, - 'description' => ArrayHelper::getValue($queryParams, 'description', $client->description), + 'description' => $request->getQueryParams()['description'] ?? $client->description, ], 'session' => [ - 'scopes' => $this->fixScopesNames(array_keys($scopes)), + 'scopes' => $this->buildScopesArray($scopes), ], ]; } - private function fixScopesNames(array $scopes): array { - foreach ($scopes as &$scope) { - if (isset(self::INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES[$scope])) { - $scope = self::INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES[$scope]; - } + /** + * @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes + * @return array + */ + private function buildScopesArray(array $scopes): array { + $result = []; + foreach ($scopes as $scope) { + $result[] = self::INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES[$scope->getIdentifier()] ?? $scope->getIdentifier(); } - return $scopes; + return $result; } - private function buildErrorResponse(OAuthException $e): array { + private function buildErrorResponse(OAuthServerException $e): array { $response = [ 'success' => false, - 'error' => $e->errorType, - 'parameter' => $e->parameter, - 'statusCode' => $e->httpStatusCode, + 'error' => $e->getErrorType(), + // 'parameter' => $e->parameter, // TODO: if this is necessary, the parameter can be extracted from the hint + 'statusCode' => $e->getHttpStatusCode(), ]; - if ($e->shouldRedirect()) { + if ($e->hasRedirect()) { $response['redirectUri'] = $e->getRedirectUri(); } - if ($e->httpStatusCode !== 200) { - Yii::$app->response->setStatusCode($e->httpStatusCode); + if ($e->getHttpStatusCode() !== 200) { + Yii::$app->response->setStatusCode($e->getHttpStatusCode()); } return $response; } - private function getGrant(string $grantType = null): GrantTypeInterface { - return $this->server->getGrantType($grantType ?? Yii::$app->request->get('grant_type')); + private function getRequest(): ServerRequestInterface { + return ServerRequest::fromGlobals(); } - private function getAuthorizationCodeGrant(): AuthCodeGrant { - /** @var GrantTypeInterface $grantType */ - $grantType = $this->getGrant('authorization_code'); - if (!$grantType instanceof AuthCodeGrant) { - throw new InvalidGrantException('authorization_code grant have invalid realisation'); - } - - return $grantType; + private function createAcceptRequiredException(): OAuthServerException { + return new OAuthServerException( + 'Client must accept authentication request.', + 0, + 'accept_required', + 401 + ); } } diff --git a/api/tests/_pages/OauthRoute.php b/api/tests/_pages/OauthRoute.php index a72aeef..2e9a3ce 100644 --- a/api/tests/_pages/OauthRoute.php +++ b/api/tests/_pages/OauthRoute.php @@ -1,40 +1,72 @@ getActor()->sendGET('/api/oauth2/v1/validate', $queryParams); } + /** + * @deprecated + */ public function complete(array $queryParams = [], array $postParams = []): void { $this->getActor()->sendPOST('/api/oauth2/v1/complete?' . http_build_query($queryParams), $postParams); } + /** + * @deprecated + */ public function issueToken(array $postParams = []): void { $this->getActor()->sendPOST('/api/oauth2/v1/token', $postParams); } + /** + * @deprecated + */ public function createClient(string $type, array $postParams): void { $this->getActor()->sendPOST('/api/v1/oauth2/' . $type, $postParams); } + /** + * @deprecated + */ public function updateClient(string $clientId, array $params): void { $this->getActor()->sendPUT('/api/v1/oauth2/' . $clientId, $params); } + /** + * @deprecated + */ public function deleteClient(string $clientId): void { $this->getActor()->sendDELETE('/api/v1/oauth2/' . $clientId); } + /** + * @deprecated + */ public function resetClient(string $clientId, bool $regenerateSecret = false): void { $this->getActor()->sendPOST("/api/v1/oauth2/{$clientId}/reset" . ($regenerateSecret ? '?regenerateSecret' : '')); } + /** + * @deprecated + */ public function getClient(string $clientId): void { $this->getActor()->sendGET("/api/v1/oauth2/{$clientId}"); } + /** + * @deprecated + */ public function getPerAccount(int $accountId): void { $this->getActor()->sendGET("/api/v1/accounts/{$accountId}/oauth2/clients"); } diff --git a/api/tests/functional/_steps/OauthSteps.php b/api/tests/functional/_steps/OauthSteps.php index da3b9a6..e12d5f2 100644 --- a/api/tests/functional/_steps/OauthSteps.php +++ b/api/tests/functional/_steps/OauthSteps.php @@ -1,13 +1,15 @@ amAuthenticated(); $route = new OauthRoute($this); $route->complete([ @@ -23,21 +25,21 @@ class OauthSteps extends FunctionalTester { return $matches[1]; } - public function getAccessToken(array $permissions = []) { + public function getAccessToken(array $permissions = []): string { $authCode = $this->getAuthCode($permissions); $response = $this->issueToken($authCode); return $response['access_token']; } - public function getRefreshToken(array $permissions = []) { + public function getRefreshToken(array $permissions = []): string { $authCode = $this->getAuthCode(array_merge([S::OFFLINE_ACCESS], $permissions)); $response = $this->issueToken($authCode); return $response['refresh_token']; } - public function issueToken($authCode) { + public function issueToken($authCode): array { $route = new OauthRoute($this); $route->issueToken([ 'code' => $authCode, @@ -50,7 +52,7 @@ class OauthSteps extends FunctionalTester { return json_decode($this->grabResponse(), true); } - public function getAccessTokenByClientCredentialsGrant(array $permissions = [], $useTrusted = true) { + public function getAccessTokenByClientCredentialsGrant(array $permissions = [], $useTrusted = true): string { $route = new OauthRoute($this); $route->issueToken([ 'client_id' => $useTrusted ? 'trusted-client' : 'default-client', diff --git a/api/tests/functional/oauth/AuthCodeCest.php b/api/tests/functional/oauth/AuthCodeCest.php index d0c4efa..cab2d35 100644 --- a/api/tests/functional/oauth/AuthCodeCest.php +++ b/api/tests/functional/oauth/AuthCodeCest.php @@ -1,4 +1,6 @@ testOauthParamsValidation($I, 'validate'); - - $I->wantTo('validate and obtain information about new auth request'); - $this->route->validate($this->buildQueryParams( - 'ely', - 'http://ely.by', - 'code', - [P::MINECRAFT_SERVER_SESSION, 'account_info', 'account_email'], - 'test-state' - )); - $I->canSeeResponseCodeIs(200); - $I->canSeeResponseIsJson(); - $I->canSeeResponseContainsJson([ - 'success' => true, - 'oAuth' => [ - 'client_id' => 'ely', - 'redirect_uri' => 'http://ely.by', - 'response_type' => 'code', - 'scope' => 'minecraft_server_session,account_info,account_email', - 'state' => 'test-state', - ], - 'client' => [ - 'id' => 'ely', - 'name' => 'Ely.by', - 'description' => 'Всем знакомое елуби', - ], - 'session' => [ - 'scopes' => [ - 'minecraft_server_session', - 'account_info', - 'account_email', - ], - ], - ]); - } - - public function testValidateWithDescriptionReplaceRequest(FunctionalTester $I) { - $I->amAuthenticated(); - $I->wantTo('validate and get information with description replacement'); - $this->route->validate($this->buildQueryParams( - 'ely', - 'http://ely.by', - 'code', - null, - null, - [ - 'description' => 'all familiar eliby', - ] - )); - $I->canSeeResponseCodeIs(200); - $I->canSeeResponseIsJson(); - $I->canSeeResponseContainsJson([ - 'client' => [ - 'description' => 'all familiar eliby', - ], - ]); } public function testCompleteValidationAction(FunctionalTester $I) { diff --git a/api/tests/functional/oauth/RefreshTokenCest.php b/api/tests/functional/oauth/RefreshTokenCest.php index 480e2d9..d2dad6b 100644 --- a/api/tests/functional/oauth/RefreshTokenCest.php +++ b/api/tests/functional/oauth/RefreshTokenCest.php @@ -1,7 +1,7 @@ wantTo('validate and obtain information about new oauth request'); + $I->sendGET('/api/oauth2/v1/validate', [ + 'client_id' => 'ely', + 'redirect_uri' => 'http://ely.by', + 'response_type' => 'code', + 'scope' => 'minecraft_server_session account_info account_email', + 'state' => 'test-state', + ]); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseContainsJson([ + 'success' => true, + 'oAuth' => [ + 'client_id' => 'ely', + 'redirect_uri' => 'http://ely.by', + 'response_type' => 'code', + 'scope' => 'minecraft_server_session account_info account_email', + 'state' => 'test-state', + ], + 'client' => [ + 'id' => 'ely', + 'name' => 'Ely.by', + 'description' => 'Всем знакомое елуби', + ], + 'session' => [ + 'scopes' => [ + 'minecraft_server_session', + 'account_info', + 'account_email', + ], + ], + ]); + } + + public function completelyValidateValidRequestWithOverriddenDescription(FunctionalTester $I) { + $I->wantTo('validate and get information with description replacement'); + $I->sendGET('/api/oauth2/v1/validate', [ + 'client_id' => 'ely', + 'redirect_uri' => 'http://ely.by', + 'response_type' => 'code', + 'description' => 'all familiar eliby', + ]); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseContainsJson([ + 'client' => [ + 'description' => 'all familiar eliby', + ], + ]); + } + +} diff --git a/common/models/OauthClient.php b/common/models/OauthClient.php index 5a856b0..799e07f 100644 --- a/common/models/OauthClient.php +++ b/common/models/OauthClient.php @@ -13,13 +13,13 @@ use yii\db\ActiveRecord; * @property string $type * @property string $name * @property string $description - * @property string $redirect_uri + * @property string|null $redirect_uri * @property string $website_url * @property string $minecraft_server_ip * @property integer $account_id * @property bool $is_trusted * @property bool $is_deleted - * @property integer $created_at + * @property int $created_at * * Behaviors: * @property Account|null $account diff --git a/common/models/OauthSession.php b/common/models/OauthSession.php index cc38d7e..cb98e33 100644 --- a/common/models/OauthSession.php +++ b/common/models/OauthSession.php @@ -69,7 +69,8 @@ class OauthSession extends ActiveRecord { } public function removeRefreshToken(): void { - /** @var \api\components\OAuth2\Storage\RefreshTokenStorage $refreshTokensStorage */ + /** @var \api\components\OAuth2\Repositories\RefreshTokenStorage $refreshTokensStorage */ + // TODO: rework $refreshTokensStorage = Yii::$app->oauth->getRefreshTokenStorage(); $refreshTokensSet = $refreshTokensStorage->sessionHash($this->id); foreach ($refreshTokensSet->members() as $refreshTokenId) { diff --git a/composer.json b/composer.json index 6f06d85..fa8b7f5 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "type": "project", "minimum-stability": "stable", "require": { - "php": "^7.2", + "php": "^7.3", "ext-intl": "*", "ext-json": "*", "ext-libxml": "*", @@ -19,7 +19,7 @@ "goaop/framework": "^2.2.0", "guzzlehttp/guzzle": "^6.0.0", "lcobucci/jwt": "^3.3", - "league/oauth2-server": "^4.1", + "league/oauth2-server": "dev-adaptation", "mito/yii2-sentry": "^1.0", "nesbot/carbon": "^2.22", "paragonie/constant_time_encoding": "^2.0", @@ -54,6 +54,10 @@ { "type": "composer", "url": "https://asset-packagist.org" + }, + { + "type": "github", + "url": "https://github.com/elyby/oauth2-server.git" } ], "config": { diff --git a/composer.lock b/composer.lock index c7464af..0cfc271 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "35095ab389bcc73cacbafceffa74fb71", + "content-hash": "35a16287a6dc45c16e0553022aa34f5b", "packages": [ { "name": "bacon/bacon-qr-code", @@ -305,6 +305,69 @@ ], "time": "2018-04-13T00:48:04+00:00" }, + { + "name": "defuse/php-encryption", + "version": "v2.2.1", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/0f407c43b953d571421e0020ba92082ed5fb7620", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.4.0" + }, + "require-dev": { + "nikic/php-parser": "^2.0|^3.0|^4.0", + "phpunit/phpunit": "^4|^5" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "time": "2018-07-24T23:27:56+00:00" + }, { "name": "doctrine/annotations", "version": "v1.6.0", @@ -1215,30 +1278,37 @@ }, { "name": "league/oauth2-server", - "version": "4.1.7", + "version": "dev-adaptation", "source": { "type": "git", - "url": "https://github.com/thephpleague/oauth2-server.git", - "reference": "138524984ac472652c69399529a35b6595cf22d3" + "url": "https://github.com/elyby/oauth2-server.git", + "reference": "08e470e81a20896109704bac4b7c24781797dfc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/138524984ac472652c69399529a35b6595cf22d3", - "reference": "138524984ac472652c69399529a35b6595cf22d3", + "url": "https://api.github.com/repos/elyby/oauth2-server/zipball/08e470e81a20896109704bac4b7c24781797dfc3", + "reference": "08e470e81a20896109704bac4b7c24781797dfc3", "shasum": "" }, "require": { - "league/event": "~2.1", - "php": ">=5.4.0", - "symfony/http-foundation": "~2.4|~3.0" + "defuse/php-encryption": "^2.2.1", + "ext-json": "*", + "ext-openssl": "*", + "lcobucci/jwt": "^3.3.1", + "league/event": "^2.2", + "php": ">=7.1.0", + "psr/http-message": "^1.0.1" }, "replace": { "league/oauth2server": "*", "lncd/oauth2": "*" }, "require-dev": { - "mockery/mockery": "0.9.*", - "phpunit/phpunit": "4.3.*" + "phpstan/phpstan": "^0.11.8", + "phpstan/phpstan-phpunit": "^0.11.2", + "phpunit/phpunit": "^7.5.13 || ^8.2.3", + "roave/security-advisories": "dev-master", + "zendframework/zend-diactoros": "^2.1.2" }, "type": "library", "autoload": { @@ -1246,7 +1316,11 @@ "League\\OAuth2\\Server\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "LeagueTests\\": "tests/" + } + }, "license": [ "MIT" ], @@ -1256,14 +1330,21 @@ "email": "hello@alexbilbie.com", "homepage": "http://www.alexbilbie.com", "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" } ], "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", - "homepage": "http://oauth2.thephpleague.com/", + "homepage": "https://oauth2.thephpleague.com/", "keywords": [ - "Authentication", "api", "auth", + "auth", + "authentication", "authorisation", "authorization", "oauth", @@ -1275,7 +1356,10 @@ "secure", "server" ], - "time": "2018-06-23T16:27:31+00:00" + "support": { + "source": "https://github.com/elyby/oauth2-server/tree/adaptation" + }, + "time": "2019-08-22T21:17:49+00:00" }, { "name": "mito/yii2-sentry", @@ -1916,60 +2000,6 @@ "homepage": "https://symfony.com", "time": "2019-06-28T13:16:30+00:00" }, - { - "name": "symfony/http-foundation", - "version": "v3.4.22", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "9a81d2330ea255ded06a69b4f7fb7804836e7a05" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9a81d2330ea255ded06a69b4f7fb7804836e7a05", - "reference": "9a81d2330ea255ded06a69b4f7fb7804836e7a05", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php70": "~1.6" - }, - "require-dev": { - "symfony/expression-language": "~2.8|~3.0|~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony HttpFoundation Component", - "homepage": "https://symfony.com", - "time": "2019-01-27T09:04:14+00:00" - }, { "name": "symfony/process", "version": "v4.2.8", @@ -6653,12 +6683,13 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { + "league/oauth2-server": 20, "roave/security-advisories": 20 }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.2", + "php": "^7.3", "ext-intl": "*", "ext-json": "*", "ext-libxml": "*",