Upgrade oauth2-server to 8.0.0 version, rewrite repositories and entities, start rewriting tests. Intermediate commit [skip ci]

This commit is contained in:
ErickSkrauch 2019-08-23 11:28:04 +03:00
parent 23a220637c
commit 0b63dc2d84
33 changed files with 604 additions and 363 deletions

View File

@ -1,10 +1,13 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2;
use api\components\OAuth2\Keys\EmptyKey;
use api\components\OAuth2\Repositories;
use DateInterval;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Storage\AccessTokenInterface;
use League\OAuth2\Server\Storage\RefreshTokenInterface;
use League\OAuth2\Server\Storage\SessionInterface;
use League\OAuth2\Server\Grant;
use yii\base\Component as BaseComponent;
/**
@ -19,18 +22,27 @@ class Component extends BaseComponent {
public function getAuthServer(): AuthorizationServer {
if ($this->_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();
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace api\components\OAuth2\Entities;
use api\components\OAuth2\Storage\SessionStorage;
use api\components\OAuth2\Repositories\SessionStorage;
use ErrorException;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;

View File

@ -1,29 +1,18 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Entities;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Entities\Traits\AuthCodeTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
class AuthCodeEntity extends \League\OAuth2\Server\Entity\AuthCodeEntity {
class AuthCodeEntity implements AuthCodeEntityInterface {
use EntityTrait;
use AuthCodeTrait;
use TokenEntityTrait;
protected $sessionId;
public function getSessionId() {
return $this->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
}

View File

@ -1,32 +1,21 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Entities;
class ClientEntity extends \League\OAuth2\Server\Entity\ClientEntity {
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\Traits\ClientTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
private $isTrusted;
class ClientEntity implements ClientEntityInterface {
use EntityTrait;
use ClientTrait;
public function setId(string $id) {
$this->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;
}
}

View File

@ -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;
}

View File

@ -1,10 +1,18 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Entities;
class ScopeEntity extends \League\OAuth2\Server\Entity\ScopeEntity {
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\ScopeTrait;
public function setId(string $id) {
$this->id = $id;
class ScopeEntity implements ScopeEntityInterface {
use EntityTrait;
use ScopeTrait;
public function __construct(string $id) {
$this->identifier = $id;
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace api\components\OAuth2\Exception;
use League\OAuth2\Server\Exception\OAuthException;
class AcceptRequiredException extends OAuthException {
public $httpStatusCode = 401;
/**
* {@inheritdoc}
*/
public $errorType = 'accept_required';
/**
* {@inheritdoc}
*/
public function __construct() {
parent::__construct('Client must accept authentication request.');
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace api\components\OAuth2\Exception;
class AccessDeniedException extends \League\OAuth2\Server\Exception\AccessDeniedException {
public function __construct($redirectUri = null) {
parent::__construct();
$this->redirectUri = $redirectUri;
}
}

View File

@ -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;

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Keys;
use League\OAuth2\Server\CryptKeyInterface;
class EmptyKey implements CryptKeyInterface {
public function getKeyPath(): string {
return '';
}
public function getPassPhrase(): ?string {
return '';
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Repositories;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
class AccessTokenRepository implements AccessTokenRepositoryInterface {
/**
* Create a new access token
*
* @param ClientEntityInterface $clientEntity
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface $scopes
* @param mixed $userIdentifier
*
* @return AccessTokenEntityInterface
*/
public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null) {
// TODO: Implement getNewToken() method.
}
/**
* Persists a new access token to permanent storage.
*
* @param AccessTokenEntityInterface $accessTokenEntity
*
* @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
*/
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity) {
// TODO: Implement persistNewAccessToken() method.
}
/**
* Revoke an access token.
*
* @param string $tokenId
*/
public function revokeAccessToken($tokenId) {
// TODO: Implement revokeAccessToken() method.
}
/**
* Check if the access token has been revoked.
*
* @param string $tokenId
*
* @return bool Return true if this token has been revoked
*/
public function isAccessTokenRevoked($tokenId) {
// TODO: Implement isAccessTokenRevoked() method.
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace api\components\OAuth2\Storage;
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\AccessTokenEntity;
use common\components\Redis\Key;

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\AuthCodeEntity;
use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
class AuthCodeRepository implements AuthCodeRepositoryInterface {
public function getNewAuthCode(): AuthCodeEntityInterface {
return new AuthCodeEntity();
}
public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity): void {
}
public function revokeAuthCode($codeId): void {
}
public function isAuthCodeRevoked($codeId): bool {
return false;
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace api\components\OAuth2\Storage;
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\AuthCodeEntity;
use common\components\Redis\Key;

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\ClientEntity;
use common\models\OauthClient;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
class ClientRepository implements ClientRepositoryInterface {
public function getClientEntity($clientId): ?ClientEntityInterface {
$client = $this->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]);
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace api\components\OAuth2\Storage;
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\ClientEntity;
use api\components\OAuth2\Entities\SessionEntity;

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Repositories;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
class RefreshTokenRepository implements RefreshTokenRepositoryInterface {
/**
* Creates a new refresh token
*
* @return RefreshTokenEntityInterface|null
*/
public function getNewRefreshToken(): RefreshTokenEntityInterface {
// TODO: Implement getNewRefreshToken() method.
}
/**
* Create a new refresh token_name.
*
* @param RefreshTokenEntityInterface $refreshTokenEntity
*
* @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
*/
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity) {
// TODO: Implement persistNewRefreshToken() method.
}
/**
* Revoke the refresh token.
*
* @param string $tokenId
*/
public function revokeRefreshToken($tokenId) {
// TODO: Implement revokeRefreshToken() method.
}
/**
* Check if the refresh token has been revoked.
*
* @param string $tokenId
*
* @return bool Return true if this token has been revoked
*/
public function isRefreshTokenRevoked($tokenId) {
// TODO: Implement isRefreshTokenRevoked() method.
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace api\components\OAuth2\Storage;
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\RefreshTokenEntity;
use common\components\Redis\Key;

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\ScopeEntity;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
class ScopeRepository implements ScopeRepositoryInterface {
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
// TODO: validate not exists scopes
return new ScopeEntity($identifier);
}
/**
* Given a client, grant type and optional user identifier validate the set of scopes requested are valid and optionally
* append additional scopes or remove requested scopes.
*
* @param ScopeEntityInterface $scopes
* @param string $grantType
* @param \League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity
* @param null|string $userIdentifier
*
* @return ScopeEntityInterface
*/
public function finalizeScopes(
array $scopes,
$grantType,
\League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity,
$userIdentifier = null
): array {
// TODO: Implement finalizeScopes() method.
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace api\components\OAuth2\Storage;
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\ClientEntity;
use api\components\OAuth2\Entities\ScopeEntity;

View File

@ -1,5 +1,5 @@
<?php
namespace api\components\OAuth2\Storage;
namespace api\components\OAuth2\Repositories;
use api\components\OAuth2\Entities\AuthCodeEntity;
use api\components\OAuth2\Entities\SessionEntity;

View File

@ -28,6 +28,7 @@ class OAuth2Identity implements IdentityInterface {
*/
public static function findIdentityByAccessToken($token, $type = null): IdentityInterface {
/** @var AccessTokenEntity|null $model */
// TODO: rework
$model = Yii::$app->oauth->getAccessTokenStorage()->get($token);
if ($model === null) {
throw new UnauthorizedHttpException('Incorrect token');

View File

@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace api\modules\oauth\controllers;
use api\controllers\Controller;
@ -55,10 +57,7 @@ class AuthorizationController extends Controller {
}
private function createOauthProcess(): OauthProcess {
$server = Yii::$app->oauth->authServer;
$server->setRequest(null); // Enforce request recreation (test environment bug)
return new OauthProcess($server);
return new OauthProcess(Yii::$app->oauth->authServer);
}
}

View File

@ -1,19 +1,18 @@
<?php
declare(strict_types=1);
namespace api\modules\oauth\models;
use api\components\OAuth2\Exception\AcceptRequiredException;
use api\components\OAuth2\Exception\AccessDeniedException;
use api\components\OAuth2\Grants\AuthCodeGrant;
use api\components\OAuth2\Grants\AuthorizeParams;
use api\rbac\Permissions as P;
use common\models\Account;
use common\models\OauthClient;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\ServerRequest;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\InvalidGrantException;
use League\OAuth2\Server\Exception\OAuthException;
use League\OAuth2\Server\Grant\GrantTypeInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use Psr\Http\Message\ServerRequestInterface;
use Yii;
use yii\helpers\ArrayHelper;
class OauthProcess {
@ -50,16 +49,13 @@ class OauthProcess {
*/
public function validate(): array {
try {
$authParams = $this->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
);
}
}

View File

@ -1,40 +1,72 @@
<?php
declare(strict_types=1);
namespace api\tests\_pages;
/**
* @deprecated
*/
class OauthRoute extends BasePage {
/**
* @deprecated
*/
public function validate(array $queryParams): void {
$this->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");
}

View File

@ -1,13 +1,15 @@
<?php
declare(strict_types=1);
namespace api\tests\functional\_steps;
use api\components\OAuth2\Storage\ScopeStorage as S;
use api\components\OAuth2\Repositories\ScopeStorage as S;
use api\tests\_pages\OauthRoute;
use api\tests\FunctionalTester;
class OauthSteps extends FunctionalTester {
public function getAuthCode(array $permissions = []) {
public function getAuthCode(array $permissions = []): string {
$this->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',

View File

@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace api\tests\functional\oauth;
use api\rbac\Permissions as P;
@ -18,61 +20,6 @@ class AuthCodeCest {
public function testValidateRequest(FunctionalTester $I) {
$this->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) {

View File

@ -1,7 +1,7 @@
<?php
namespace api\tests\functional\oauth;
use api\components\OAuth2\Storage\ScopeStorage as S;
use api\components\OAuth2\Repositories\ScopeStorage as S;
use api\rbac\Permissions as P;
use api\tests\_pages\OauthRoute;
use api\tests\functional\_steps\OauthSteps;

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace api\tests\functional\oauth;
use api\tests\FunctionalTester;
class ValidateCest {
// TODO: validate case, when scopes are passed with commas
public function completelyValidateValidRequest(FunctionalTester $I) {
$I->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',
],
]);
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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": {

171
composer.lock generated
View File

@ -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": "*",