mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +05:30
Upgrade oauth2-server to 8.0.0 version, rewrite repositories and entities, start rewriting tests. Intermediate commit [skip ci]
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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.');
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
|
18
api/components/OAuth2/Keys/EmptyKey.php
Normal file
18
api/components/OAuth2/Keys/EmptyKey.php
Normal 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 '';
|
||||
}
|
||||
|
||||
}
|
56
api/components/OAuth2/Repositories/AccessTokenRepository.php
Normal file
56
api/components/OAuth2/Repositories/AccessTokenRepository.php
Normal 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.
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
26
api/components/OAuth2/Repositories/AuthCodeRepository.php
Normal file
26
api/components/OAuth2/Repositories/AuthCodeRepository.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
41
api/components/OAuth2/Repositories/ClientRepository.php
Normal file
41
api/components/OAuth2/Repositories/ClientRepository.php
Normal 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]);
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
@@ -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.
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
37
api/components/OAuth2/Repositories/ScopeRepository.php
Normal file
37
api/components/OAuth2/Repositories/ScopeRepository.php
Normal 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.
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
@@ -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;
|
@@ -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');
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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");
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
62
api/tests/functional/oauth/ValidateCest.php
Normal file
62
api/tests/functional/oauth/ValidateCest.php
Normal 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',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user