mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +05:30
Merge branch 'oauth_jwt_tokens' into 'master'
Make every auth token JWT See merge request elyby/accounts!9
This commit is contained in:
@@ -7,8 +7,10 @@ EMAILS_RENDERER_HOST=http://emails-renderer:3000
|
||||
|
||||
## Security params
|
||||
JWT_USER_SECRET=
|
||||
JWT_ENCRYPTION_KEY=
|
||||
JWT_PUBLIC_KEY_PATH=
|
||||
JWT_PRIVATE_KEY_PATH=
|
||||
JWT_PRIVATE_KEY_PASS=
|
||||
|
||||
## External services
|
||||
RECAPTCHA_PUBLIC=
|
||||
|
@@ -1,15 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2;
|
||||
|
||||
use Carbon\CarbonInterval;
|
||||
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 yii\base\Component as BaseComponent;
|
||||
|
||||
/**
|
||||
* @property AuthorizationServer $authServer
|
||||
*/
|
||||
class Component extends BaseComponent {
|
||||
|
||||
/**
|
||||
@@ -19,35 +17,45 @@ 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
|
||||
|
||||
$authServer->addGrantType(new Grants\AuthCodeGrant());
|
||||
$authServer->addGrantType(new Grants\RefreshTokenGrant());
|
||||
$authServer->addGrantType(new Grants\ClientCredentialsGrant());
|
||||
|
||||
$this->_authServer = $authServer;
|
||||
$this->_authServer = $this->createAuthServer();
|
||||
}
|
||||
|
||||
return $this->_authServer;
|
||||
}
|
||||
|
||||
public function getAccessTokenStorage(): AccessTokenInterface {
|
||||
return $this->getAuthServer()->getAccessTokenStorage();
|
||||
}
|
||||
private function createAuthServer(): AuthorizationServer {
|
||||
$clientsRepo = new Repositories\ClientRepository();
|
||||
$accessTokensRepo = new Repositories\AccessTokenRepository();
|
||||
$publicScopesRepo = new Repositories\PublicScopeRepository();
|
||||
$internalScopesRepo = new Repositories\InternalScopeRepository();
|
||||
$authCodesRepo = new Repositories\AuthCodeRepository();
|
||||
$refreshTokensRepo = new Repositories\RefreshTokenRepository();
|
||||
|
||||
public function getRefreshTokenStorage(): RefreshTokenInterface {
|
||||
return $this->getAuthServer()->getRefreshTokenStorage();
|
||||
}
|
||||
$accessTokenTTL = CarbonInterval::create(-1); // Set negative value to make tokens non expiring
|
||||
|
||||
public function getSessionStorage(): SessionInterface {
|
||||
return $this->getAuthServer()->getSessionStorage();
|
||||
$authServer = new AuthorizationServer(
|
||||
$clientsRepo,
|
||||
$accessTokensRepo,
|
||||
new Repositories\EmptyScopeRepository(),
|
||||
new Keys\EmptyKey(),
|
||||
'', // Omit the key because we use our own encryption mechanism
|
||||
new ResponseTypes\BearerTokenResponse()
|
||||
);
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$authCodeGrant = new Grants\AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M'));
|
||||
$authCodeGrant->disableRequireCodeChallengeForPublicClients();
|
||||
$authServer->enableGrantType($authCodeGrant, $accessTokenTTL);
|
||||
$authCodeGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||
|
||||
$refreshTokenGrant = new Grants\RefreshTokenGrant($refreshTokensRepo);
|
||||
$authServer->enableGrantType($refreshTokenGrant, $accessTokenTTL);
|
||||
$refreshTokenGrant->setScopeRepository($publicScopesRepo); // Change repository after enabling
|
||||
|
||||
$clientCredentialsGrant = new Grants\ClientCredentialsGrant();
|
||||
$authServer->enableGrantType($clientCredentialsGrant, $accessTokenTTL);
|
||||
$clientCredentialsGrant->setScopeRepository($internalScopesRepo); // Change repository after enabling
|
||||
|
||||
return $authServer;
|
||||
}
|
||||
|
||||
}
|
||||
|
26
api/components/OAuth2/CryptTrait.php
Normal file
26
api/components/OAuth2/CryptTrait.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2;
|
||||
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* This trait is intended to override the standard data encryption behavior
|
||||
* with the help of \Defuse\Crypto\Crypto class, because the resultant string
|
||||
* is much larger than the original one.
|
||||
*
|
||||
* The implementation under the hood relies on using libsodium library
|
||||
* that provides more compact result values.
|
||||
*/
|
||||
trait CryptTrait {
|
||||
|
||||
protected function encrypt($unencryptedData): string {
|
||||
return Yii::$app->tokens->encryptValue($unencryptedData);
|
||||
}
|
||||
|
||||
protected function decrypt($encryptedData): string {
|
||||
return Yii::$app->tokens->decryptValue($encryptedData);
|
||||
}
|
||||
|
||||
}
|
@@ -1,44 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use api\components\OAuth2\Storage\SessionStorage;
|
||||
use ErrorException;
|
||||
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
|
||||
use League\OAuth2\Server\CryptKeyInterface;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
|
||||
use Yii;
|
||||
|
||||
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
|
||||
class AccessTokenEntity implements AccessTokenEntityInterface {
|
||||
use EntityTrait;
|
||||
use TokenEntityTrait;
|
||||
|
||||
protected $sessionId;
|
||||
|
||||
public function getSessionId() {
|
||||
return $this->sessionId;
|
||||
public function __toString(): string {
|
||||
return (string)Yii::$app->tokensFactory->createForOAuthClient($this);
|
||||
}
|
||||
|
||||
public function setSessionId($sessionId) {
|
||||
$this->sessionId = $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @return static
|
||||
*/
|
||||
public function setSession(OriginalSessionEntity $session) {
|
||||
parent::setSession($session);
|
||||
$this->sessionId = $session->getId();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSession(): ?OriginalSessionEntity {
|
||||
if ($this->session instanceof OriginalSessionEntity) {
|
||||
return $this->session;
|
||||
}
|
||||
|
||||
$sessionStorage = $this->server->getSessionStorage();
|
||||
if (!$sessionStorage instanceof SessionStorage) {
|
||||
throw new ErrorException('SessionStorage must be instance of ' . SessionStorage::class);
|
||||
}
|
||||
|
||||
return $sessionStorage->getById($this->sessionId);
|
||||
public function setPrivateKey(CryptKeyInterface $privateKey): void {
|
||||
// We use a general-purpose component to build JWT tokens, so there is no need to keep the key
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,29 +1,16 @@
|
||||
<?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 {
|
||||
|
||||
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;
|
||||
}
|
||||
class AuthCodeEntity implements AuthCodeEntityInterface {
|
||||
use EntityTrait;
|
||||
use AuthCodeTrait;
|
||||
use TokenEntityTrait;
|
||||
|
||||
}
|
||||
|
@@ -1,28 +1,30 @@
|
||||
<?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;
|
||||
|
||||
class ClientEntity implements ClientEntityInterface {
|
||||
use EntityTrait;
|
||||
use ClientTrait;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isTrusted;
|
||||
|
||||
public function setId(string $id) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function setName(string $name) {
|
||||
public function __construct(string $id, string $name, $redirectUri, bool $isTrusted) {
|
||||
$this->identifier = $id;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function setSecret(string $secret) {
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
public function setRedirectUri($redirectUri) {
|
||||
$this->redirectUri = $redirectUri;
|
||||
$this->isTrusted = $isTrusted;
|
||||
}
|
||||
|
||||
public function setIsTrusted(bool $isTrusted) {
|
||||
$this->isTrusted = $isTrusted;
|
||||
public function isConfidential(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isTrusted(): bool {
|
||||
|
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@@ -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,27 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use League\OAuth2\Server\Entity\ClientEntity as OriginalClientEntity;
|
||||
use League\OAuth2\Server\Entity\EntityTrait;
|
||||
|
||||
class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
|
||||
use EntityTrait;
|
||||
|
||||
protected $clientId;
|
||||
|
||||
public function getClientId() {
|
||||
return $this->clientId;
|
||||
}
|
||||
|
||||
public function associateClient(OriginalClientEntity $client) {
|
||||
parent::associateClient($client);
|
||||
$this->clientId = $client->getId();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setClientId(string $clientId) {
|
||||
$this->clientId = $clientId;
|
||||
}
|
||||
|
||||
}
|
16
api/components/OAuth2/Entities/UserEntity.php
Normal file
16
api/components/OAuth2/Entities/UserEntity.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use League\OAuth2\Server\Entities\UserEntityInterface;
|
||||
|
||||
class UserEntity implements UserEntityInterface {
|
||||
use EntityTrait;
|
||||
|
||||
public function __construct(int $id) {
|
||||
$this->identifier = $id;
|
||||
}
|
||||
|
||||
}
|
10
api/components/OAuth2/Events/RequestedRefreshToken.php
Normal file
10
api/components/OAuth2/Events/RequestedRefreshToken.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Events;
|
||||
|
||||
use League\Event\AbstractEvent;
|
||||
|
||||
class RequestedRefreshToken extends AbstractEvent {
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,239 +1,43 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
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\Utils\Scopes;
|
||||
use League\OAuth2\Server\Entity\AuthCodeEntity as BaseAuthCodeEntity;
|
||||
use League\OAuth2\Server\Entity\ClientEntity as BaseClientEntity;
|
||||
use League\OAuth2\Server\Event\ClientAuthenticationFailedEvent;
|
||||
use League\OAuth2\Server\Exception;
|
||||
use League\OAuth2\Server\Grant\AbstractGrant;
|
||||
use League\OAuth2\Server\Util\SecureKey;
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use api\components\OAuth2\Events\RequestedRefreshToken;
|
||||
use api\components\OAuth2\Repositories\PublicScopeRepository;
|
||||
use DateInterval;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Grant\AuthCodeGrant as BaseAuthCodeGrant;
|
||||
|
||||
class AuthCodeGrant extends AbstractGrant {
|
||||
|
||||
protected $identifier = 'authorization_code';
|
||||
|
||||
protected $responseType = 'code';
|
||||
|
||||
protected $authTokenTTL = 600;
|
||||
|
||||
protected $requireClientSecret = true;
|
||||
|
||||
public function setAuthTokenTTL(int $authTokenTTL): void {
|
||||
$this->authTokenTTL = $authTokenTTL;
|
||||
}
|
||||
|
||||
public function setRequireClientSecret(bool $required): void {
|
||||
$this->requireClientSecret = $required;
|
||||
}
|
||||
|
||||
public function shouldRequireClientSecret(): bool {
|
||||
return $this->requireClientSecret;
|
||||
}
|
||||
class AuthCodeGrant extends BaseAuthCodeGrant {
|
||||
use CryptTrait;
|
||||
|
||||
/**
|
||||
* Check authorize parameters
|
||||
* @param DateInterval $accessTokenTTL
|
||||
* @param ClientEntityInterface $client
|
||||
* @param string|null $userIdentifier
|
||||
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes
|
||||
*
|
||||
* @return AuthorizeParams Authorize request parameters
|
||||
* @throws Exception\OAuthException
|
||||
*
|
||||
* @throws
|
||||
* @return AccessTokenEntityInterface
|
||||
* @throws \League\OAuth2\Server\Exception\OAuthServerException
|
||||
* @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
|
||||
*/
|
||||
public function checkAuthorizeParams(): AuthorizeParams {
|
||||
// Get required params
|
||||
$clientId = $this->server->getRequest()->query->get('client_id');
|
||||
if ($clientId === null) {
|
||||
throw new Exception\InvalidRequestException('client_id');
|
||||
protected function issueAccessToken(
|
||||
DateInterval $accessTokenTTL,
|
||||
ClientEntityInterface $client,
|
||||
$userIdentifier,
|
||||
array $scopes = []
|
||||
): AccessTokenEntityInterface {
|
||||
foreach ($scopes as $i => $scope) {
|
||||
if ($scope->getIdentifier() === PublicScopeRepository::OFFLINE_ACCESS) {
|
||||
unset($scopes[$i]);
|
||||
$this->getEmitter()->emit(new RequestedRefreshToken());
|
||||
}
|
||||
}
|
||||
|
||||
$redirectUri = $this->server->getRequest()->query->get('redirect_uri');
|
||||
if ($redirectUri === null) {
|
||||
throw new Exception\InvalidRequestException('redirect_uri');
|
||||
}
|
||||
|
||||
// Validate client ID and redirect URI
|
||||
$client = $this->server->getClientStorage()->get($clientId, null, $redirectUri, $this->getIdentifier());
|
||||
if (!$client instanceof ClientEntity) {
|
||||
$this->server->getEventEmitter()->emit(new ClientAuthenticationFailedEvent($this->server->getRequest()));
|
||||
throw new Exception\InvalidClientException();
|
||||
}
|
||||
|
||||
$state = $this->server->getRequest()->query->get('state');
|
||||
if ($state === null && $this->server->stateParamRequired()) {
|
||||
throw new Exception\InvalidRequestException('state', $redirectUri);
|
||||
}
|
||||
|
||||
$responseType = $this->server->getRequest()->query->get('response_type');
|
||||
if ($responseType === null) {
|
||||
throw new Exception\InvalidRequestException('response_type', $redirectUri);
|
||||
}
|
||||
|
||||
// Ensure response type is one that is recognised
|
||||
if (!in_array($responseType, $this->server->getResponseTypes(), true)) {
|
||||
throw new Exception\UnsupportedResponseTypeException($responseType, $redirectUri);
|
||||
}
|
||||
|
||||
// Validate any scopes that are in the request
|
||||
$scopeParam = $this->server->getRequest()->query->get('scope', '');
|
||||
$scopes = $this->validateScopes($scopeParam, $client, $redirectUri);
|
||||
|
||||
return new AuthorizeParams($client, $redirectUri, $state, $responseType, $scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a new authorize request
|
||||
*
|
||||
* @param string $type The session owner's type
|
||||
* @param string $typeId The session owner's ID
|
||||
* @param AuthorizeParams $authParams The authorize request $_GET parameters
|
||||
*
|
||||
* @return string An authorisation code
|
||||
*/
|
||||
public function newAuthorizeRequest(string $type, string $typeId, AuthorizeParams $authParams): string {
|
||||
// Create a new session
|
||||
$session = new SessionEntity($this->server);
|
||||
$session->setOwner($type, $typeId);
|
||||
$session->associateClient($authParams->getClient());
|
||||
|
||||
// Create a new auth code
|
||||
$authCode = new AuthCodeEntity($this->server);
|
||||
$authCode->setId(SecureKey::generate());
|
||||
$authCode->setRedirectUri($authParams->getRedirectUri());
|
||||
$authCode->setExpireTime(time() + $this->authTokenTTL);
|
||||
|
||||
foreach ($authParams->getScopes() as $scope) {
|
||||
$authCode->associateScope($scope);
|
||||
$session->associateScope($scope);
|
||||
}
|
||||
|
||||
$session->save();
|
||||
$authCode->setSession($session);
|
||||
$authCode->save();
|
||||
|
||||
return $authCode->generateRedirectUri($authParams->getState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the auth code grant
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception\OAuthException
|
||||
*/
|
||||
public function completeFlow(): array {
|
||||
// Get the required params
|
||||
$clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
|
||||
if ($clientId === null) {
|
||||
throw new Exception\InvalidRequestException('client_id');
|
||||
}
|
||||
|
||||
$clientSecret = $this->server->getRequest()->request->get(
|
||||
'client_secret',
|
||||
$this->server->getRequest()->getPassword()
|
||||
);
|
||||
if ($clientSecret === null && $this->shouldRequireClientSecret()) {
|
||||
throw new Exception\InvalidRequestException('client_secret');
|
||||
}
|
||||
|
||||
$redirectUri = $this->server->getRequest()->request->get('redirect_uri');
|
||||
if ($redirectUri === null) {
|
||||
throw new Exception\InvalidRequestException('redirect_uri');
|
||||
}
|
||||
|
||||
// Validate client ID and client secret
|
||||
$client = $this->server->getClientStorage()->get($clientId, $clientSecret, $redirectUri, $this->getIdentifier());
|
||||
if (!$client instanceof BaseClientEntity) {
|
||||
$this->server->getEventEmitter()->emit(new ClientAuthenticationFailedEvent($this->server->getRequest()));
|
||||
throw new Exception\InvalidClientException();
|
||||
}
|
||||
|
||||
// Validate the auth code
|
||||
$authCode = $this->server->getRequest()->request->get('code');
|
||||
if ($authCode === null) {
|
||||
throw new Exception\InvalidRequestException('code');
|
||||
}
|
||||
|
||||
$code = $this->server->getAuthCodeStorage()->get($authCode);
|
||||
if (($code instanceof BaseAuthCodeEntity) === false) {
|
||||
throw new Exception\InvalidRequestException('code');
|
||||
}
|
||||
|
||||
// Ensure the auth code hasn't expired
|
||||
if ($code->isExpired()) {
|
||||
throw new Exception\InvalidRequestException('code');
|
||||
}
|
||||
|
||||
// Check redirect URI presented matches redirect URI originally used in authorize request
|
||||
if ($code->getRedirectUri() !== $redirectUri) {
|
||||
throw new Exception\InvalidRequestException('redirect_uri');
|
||||
}
|
||||
|
||||
$session = $code->getSession();
|
||||
$session->associateClient($client);
|
||||
|
||||
$authCodeScopes = $code->getScopes();
|
||||
|
||||
// Generate the access token
|
||||
$accessToken = new AccessTokenEntity($this->server);
|
||||
$accessToken->setId(SecureKey::generate());
|
||||
$accessToken->setExpireTime($this->getAccessTokenTTL() + time());
|
||||
|
||||
foreach ($authCodeScopes as $authCodeScope) {
|
||||
$session->associateScope($authCodeScope);
|
||||
}
|
||||
|
||||
foreach ($session->getScopes() as $scope) {
|
||||
$accessToken->associateScope($scope);
|
||||
}
|
||||
|
||||
$this->server->getTokenType()->setSession($session);
|
||||
$this->server->getTokenType()->setParam('access_token', $accessToken->getId());
|
||||
$this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
|
||||
|
||||
// Set refresh_token param only in case when offline_access requested
|
||||
if (isset($accessToken->getScopes()[ScopeStorage::OFFLINE_ACCESS])) {
|
||||
/** @var RefreshTokenGrant $refreshTokenGrant */
|
||||
$refreshTokenGrant = $this->server->getGrantType('refresh_token');
|
||||
$refreshToken = new RefreshTokenEntity($this->server);
|
||||
$refreshToken->setId(SecureKey::generate());
|
||||
$refreshToken->setExpireTime($refreshTokenGrant->getRefreshTokenTTL() + time());
|
||||
$this->server->getTokenType()->setParam('refresh_token', $refreshToken->getId());
|
||||
}
|
||||
|
||||
// Expire the auth code
|
||||
$code->expire();
|
||||
|
||||
// Save all the things
|
||||
$accessToken->setSession($session);
|
||||
$accessToken->save();
|
||||
|
||||
if (isset($refreshToken)) {
|
||||
$refreshToken->setAccessToken($accessToken);
|
||||
$refreshToken->save();
|
||||
}
|
||||
|
||||
return $this->server->getTokenType()->generateResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* In the earlier versions of Accounts Ely.by backend we had a comma-separated scopes
|
||||
* list, while by OAuth2 standard it they should be separated by a space. Shit happens :)
|
||||
* So override scopes validation function to reformat passed value.
|
||||
*
|
||||
* @param string $scopeParam
|
||||
* @param BaseClientEntity $client
|
||||
* @param string $redirectUri
|
||||
*
|
||||
* @return \League\OAuth2\Server\Entity\ScopeEntity[]
|
||||
*/
|
||||
public function validateScopes($scopeParam = '', BaseClientEntity $client, $redirectUri = null) {
|
||||
return parent::validateScopes(Scopes::format($scopeParam), $client, $redirectUri);
|
||||
return parent::issueAccessToken($accessTokenTTL, $client, $userIdentifier, $scopes);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
|
||||
class AuthorizeParams {
|
||||
|
||||
private $client;
|
||||
|
||||
private $redirectUri;
|
||||
|
||||
private $state;
|
||||
|
||||
private $responseType;
|
||||
|
||||
/**
|
||||
* @var \api\components\OAuth2\Entities\ScopeEntity[]
|
||||
*/
|
||||
private $scopes;
|
||||
|
||||
public function __construct(
|
||||
ClientEntity $client,
|
||||
string $redirectUri,
|
||||
?string $state,
|
||||
string $responseType,
|
||||
array $scopes
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->redirectUri = $redirectUri;
|
||||
$this->state = $state;
|
||||
$this->responseType = $responseType;
|
||||
$this->scopes = $scopes;
|
||||
}
|
||||
|
||||
public function getClient(): ClientEntity {
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function getRedirectUri(): string {
|
||||
return $this->redirectUri;
|
||||
}
|
||||
|
||||
public function getState(): ?string {
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function getResponseType(): string {
|
||||
return $this->responseType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \api\components\OAuth2\Entities\ScopeEntity[]
|
||||
*/
|
||||
public function getScopes(): array {
|
||||
return $this->scopes ?? [];
|
||||
}
|
||||
|
||||
}
|
@@ -1,86 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\components\OAuth2\Entities\SessionEntity;
|
||||
use api\components\OAuth2\Utils\Scopes;
|
||||
use League\OAuth2\Server\Entity\ClientEntity as BaseClientEntity;
|
||||
use League\OAuth2\Server\Event;
|
||||
use League\OAuth2\Server\Exception;
|
||||
use League\OAuth2\Server\Grant\AbstractGrant;
|
||||
use League\OAuth2\Server\Util\SecureKey;
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use League\OAuth2\Server\Grant\ClientCredentialsGrant as BaseClientCredentialsGrant;
|
||||
|
||||
class ClientCredentialsGrant extends AbstractGrant {
|
||||
|
||||
protected $identifier = 'client_credentials';
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \League\OAuth2\Server\Exception\OAuthException
|
||||
*/
|
||||
public function completeFlow(): array {
|
||||
$clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
|
||||
if ($clientId === null) {
|
||||
throw new Exception\InvalidRequestException('client_id');
|
||||
}
|
||||
|
||||
$clientSecret = $this->server->getRequest()->request->get('client_secret');
|
||||
if ($clientSecret === null) {
|
||||
throw new Exception\InvalidRequestException('client_secret');
|
||||
}
|
||||
|
||||
// Validate client ID and client secret
|
||||
$client = $this->server->getClientStorage()->get($clientId, $clientSecret, null, $this->getIdentifier());
|
||||
if (!$client instanceof BaseClientEntity) {
|
||||
$this->server->getEventEmitter()->emit(new Event\ClientAuthenticationFailedEvent($this->server->getRequest()));
|
||||
throw new Exception\InvalidClientException();
|
||||
}
|
||||
|
||||
// Validate any scopes that are in the request
|
||||
$scopeParam = $this->server->getRequest()->request->get('scope', '');
|
||||
$scopes = $this->validateScopes($scopeParam, $client);
|
||||
|
||||
// Create a new session
|
||||
$session = new SessionEntity($this->server);
|
||||
$session->setOwner('client', $client->getId());
|
||||
$session->associateClient($client);
|
||||
|
||||
// Generate an access token
|
||||
$accessToken = new AccessTokenEntity($this->server);
|
||||
$accessToken->setId(SecureKey::generate());
|
||||
$accessToken->setExpireTime($this->getAccessTokenTTL() + time());
|
||||
|
||||
// Associate scopes with the session and access token
|
||||
foreach ($scopes as $scope) {
|
||||
$session->associateScope($scope);
|
||||
$accessToken->associateScope($scope);
|
||||
}
|
||||
|
||||
// Save everything
|
||||
$session->save();
|
||||
$accessToken->setSession($session);
|
||||
$accessToken->save();
|
||||
|
||||
$this->server->getTokenType()->setSession($session);
|
||||
$this->server->getTokenType()->setParam('access_token', $accessToken->getId());
|
||||
$this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
|
||||
|
||||
return $this->server->getTokenType()->generateResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* In the earlier versions of Accounts Ely.by backend we had a comma-separated scopes
|
||||
* list, while by OAuth2 standard it they should be separated by a space. Shit happens :)
|
||||
* So override scopes validation function to reformat passed value.
|
||||
*
|
||||
* @param string $scopeParam
|
||||
* @param BaseClientEntity $client
|
||||
* @param string $redirectUri
|
||||
*
|
||||
* @return \League\OAuth2\Server\Entity\ScopeEntity[]
|
||||
*/
|
||||
public function validateScopes($scopeParam = '', BaseClientEntity $client, $redirectUri = null) {
|
||||
return parent::validateScopes(Scopes::format($scopeParam), $client, $redirectUri);
|
||||
}
|
||||
class ClientCredentialsGrant extends BaseClientCredentialsGrant {
|
||||
use CryptTrait;
|
||||
|
||||
}
|
||||
|
@@ -1,187 +1,123 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Grants;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\components\OAuth2\Entities\RefreshTokenEntity;
|
||||
use api\components\OAuth2\Utils\Scopes;
|
||||
use ErrorException;
|
||||
use League\OAuth2\Server\Entity\AccessTokenEntity as BaseAccessTokenEntity;
|
||||
use League\OAuth2\Server\Entity\ClientEntity as BaseClientEntity;
|
||||
use League\OAuth2\Server\Entity\RefreshTokenEntity as BaseRefreshTokenEntity;
|
||||
use League\OAuth2\Server\Event\ClientAuthenticationFailedEvent;
|
||||
use League\OAuth2\Server\Exception;
|
||||
use League\OAuth2\Server\Grant\AbstractGrant;
|
||||
use League\OAuth2\Server\Util\SecureKey;
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use api\components\Tokens\TokenReader;
|
||||
use Carbon\Carbon;
|
||||
use common\models\OauthSession;
|
||||
use InvalidArgumentException;
|
||||
use Lcobucci\JWT\ValidationData;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Grant\RefreshTokenGrant as BaseRefreshTokenGrant;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Yii;
|
||||
|
||||
class RefreshTokenGrant extends AbstractGrant {
|
||||
class RefreshTokenGrant extends BaseRefreshTokenGrant {
|
||||
use CryptTrait;
|
||||
|
||||
protected $identifier = 'refresh_token';
|
||||
/**
|
||||
* Previously, refresh tokens were stored in Redis.
|
||||
* If received refresh token is matches the legacy token template,
|
||||
* restore the information from the legacy storage.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param string $clientId
|
||||
*
|
||||
* @return array
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId): array {
|
||||
$refreshToken = $this->getRequestParameter('refresh_token', $request);
|
||||
if ($refreshToken !== null && mb_strlen($refreshToken) === 40) {
|
||||
return $this->validateLegacyRefreshToken($refreshToken);
|
||||
}
|
||||
|
||||
protected $refreshTokenTTL = 604800;
|
||||
|
||||
protected $refreshTokenRotate = false;
|
||||
|
||||
protected $requireClientSecret = true;
|
||||
|
||||
public function setRefreshTokenTTL($refreshTokenTTL): void {
|
||||
$this->refreshTokenTTL = $refreshTokenTTL;
|
||||
}
|
||||
|
||||
public function getRefreshTokenTTL(): int {
|
||||
return $this->refreshTokenTTL;
|
||||
}
|
||||
|
||||
public function setRefreshTokenRotation(bool $refreshTokenRotate = true): void {
|
||||
$this->refreshTokenRotate = $refreshTokenRotate;
|
||||
}
|
||||
|
||||
public function shouldRotateRefreshTokens(): bool {
|
||||
return $this->refreshTokenRotate;
|
||||
}
|
||||
|
||||
public function setRequireClientSecret(string $required): void {
|
||||
$this->requireClientSecret = $required;
|
||||
}
|
||||
|
||||
public function shouldRequireClientSecret(): bool {
|
||||
return $this->requireClientSecret;
|
||||
return $this->validateAccessToken($refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* In the earlier versions of Accounts Ely.by backend we had a comma-separated scopes
|
||||
* list, while by OAuth2 standard it they should be separated by a space. Shit happens :)
|
||||
* So override scopes validation function to reformat passed value.
|
||||
* Currently we're not rotating refresh tokens.
|
||||
* So we overriding this method to always return null, which means,
|
||||
* that refresh_token will not be issued.
|
||||
*
|
||||
* @param string $scopeParam
|
||||
* @param BaseClientEntity $client
|
||||
* @param string $redirectUri
|
||||
* @param AccessTokenEntityInterface $accessToken
|
||||
*
|
||||
* @return \League\OAuth2\Server\Entity\ScopeEntity[]
|
||||
* @return RefreshTokenEntityInterface|null
|
||||
*/
|
||||
public function validateScopes($scopeParam = '', BaseClientEntity $client, $redirectUri = null) {
|
||||
return parent::validateScopes(Scopes::format($scopeParam), $client, $redirectUri);
|
||||
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The method has been overridden because we stores access_tokens in Redis with expire value,
|
||||
* so they might not exists at the moment, when it will be requested via refresh_token.
|
||||
* That's why we extends RefreshTokenEntity to give it knowledge about related session.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @throws \League\OAuth2\Server\Exception\OAuthException
|
||||
* @param string $refreshToken
|
||||
* @return array
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
public function completeFlow(): array {
|
||||
$clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
|
||||
if ($clientId === null) {
|
||||
throw new Exception\InvalidRequestException('client_id');
|
||||
private function validateLegacyRefreshToken(string $refreshToken): array {
|
||||
$result = Yii::$app->redis->get("oauth:refresh:tokens:{$refreshToken}");
|
||||
if ($result === null) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
|
||||
}
|
||||
|
||||
$clientSecret = $this->server->getRequest()->request->get(
|
||||
'client_secret',
|
||||
$this->server->getRequest()->getPassword()
|
||||
);
|
||||
if ($clientSecret === null && $this->shouldRequireClientSecret()) {
|
||||
throw new Exception\InvalidRequestException('client_secret');
|
||||
try {
|
||||
[
|
||||
'access_token_id' => $accessTokenId,
|
||||
'session_id' => $sessionId,
|
||||
] = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\Exception $e) {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
|
||||
}
|
||||
|
||||
// Validate client ID and client secret
|
||||
$client = $this->server->getClientStorage()->get($clientId, $clientSecret, null, $this->getIdentifier());
|
||||
if (($client instanceof BaseClientEntity) === false) {
|
||||
$this->server->getEventEmitter()->emit(new ClientAuthenticationFailedEvent($this->server->getRequest()));
|
||||
throw new Exception\InvalidClientException();
|
||||
/** @var OauthSession|null $relatedSession */
|
||||
$relatedSession = OauthSession::findOne(['legacy_id' => $sessionId]);
|
||||
if ($relatedSession === null) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
|
||||
}
|
||||
|
||||
$oldRefreshTokenParam = $this->server->getRequest()->request->get('refresh_token');
|
||||
if ($oldRefreshTokenParam === null) {
|
||||
throw new Exception\InvalidRequestException('refresh_token');
|
||||
return [
|
||||
'client_id' => $relatedSession->client_id,
|
||||
'refresh_token_id' => $refreshToken,
|
||||
'access_token_id' => $accessTokenId,
|
||||
'scopes' => $relatedSession->getScopes(),
|
||||
'user_id' => $relatedSession->account_id,
|
||||
'expire_time' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $jwt
|
||||
* @return array
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
private function validateAccessToken(string $jwt): array {
|
||||
try {
|
||||
$token = Yii::$app->tokens->parse($jwt);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
|
||||
}
|
||||
|
||||
// Validate refresh token
|
||||
$oldRefreshToken = $this->server->getRefreshTokenStorage()->get($oldRefreshTokenParam);
|
||||
if (($oldRefreshToken instanceof BaseRefreshTokenEntity) === false) {
|
||||
throw new Exception\InvalidRefreshException();
|
||||
if (!Yii::$app->tokens->verify($token)) {
|
||||
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token');
|
||||
}
|
||||
|
||||
// Ensure the old refresh token hasn't expired
|
||||
if ($oldRefreshToken->isExpired()) {
|
||||
throw new Exception\InvalidRefreshException();
|
||||
if (!$token->validate(new ValidationData(Carbon::now()->getTimestamp()))) {
|
||||
throw OAuthServerException::invalidRefreshToken('Token has expired');
|
||||
}
|
||||
|
||||
/** @var AccessTokenEntity|null $oldAccessToken */
|
||||
$oldAccessToken = $oldRefreshToken->getAccessToken();
|
||||
if ($oldAccessToken instanceof AccessTokenEntity) {
|
||||
// Get the scopes for the original session
|
||||
$session = $oldAccessToken->getSession();
|
||||
} else {
|
||||
if (!$oldRefreshToken instanceof RefreshTokenEntity) {
|
||||
/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
|
||||
throw new ErrorException('oldRefreshToken must be instance of ' . RefreshTokenEntity::class);
|
||||
}
|
||||
$reader = new TokenReader($token);
|
||||
|
||||
$session = $oldRefreshToken->getSession();
|
||||
}
|
||||
|
||||
if ($session === null) {
|
||||
throw new Exception\InvalidRefreshException();
|
||||
}
|
||||
|
||||
$scopes = $this->formatScopes($session->getScopes());
|
||||
|
||||
// Get and validate any requested scopes
|
||||
$requestedScopesString = $this->server->getRequest()->request->get('scope', '');
|
||||
$requestedScopes = $this->validateScopes($requestedScopesString, $client);
|
||||
|
||||
// If no new scopes are requested then give the access token the original session scopes
|
||||
if (count($requestedScopes) === 0) {
|
||||
$newScopes = $scopes;
|
||||
} else {
|
||||
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
|
||||
// the request doesn't include any new scopes
|
||||
foreach ($requestedScopes as $requestedScope) {
|
||||
if (!isset($scopes[$requestedScope->getId()])) {
|
||||
throw new Exception\InvalidScopeException($requestedScope->getId());
|
||||
}
|
||||
}
|
||||
|
||||
$newScopes = $requestedScopes;
|
||||
}
|
||||
|
||||
// Generate a new access token and assign it the correct sessions
|
||||
$newAccessToken = new AccessTokenEntity($this->server);
|
||||
$newAccessToken->setId(SecureKey::generate());
|
||||
$newAccessToken->setExpireTime($this->getAccessTokenTTL() + time());
|
||||
$newAccessToken->setSession($session);
|
||||
|
||||
foreach ($newScopes as $newScope) {
|
||||
$newAccessToken->associateScope($newScope);
|
||||
}
|
||||
|
||||
// Expire the old token and save the new one
|
||||
$oldAccessToken instanceof BaseAccessTokenEntity && $oldAccessToken->expire();
|
||||
$newAccessToken->save();
|
||||
|
||||
$this->server->getTokenType()->setSession($session);
|
||||
$this->server->getTokenType()->setParam('access_token', $newAccessToken->getId());
|
||||
$this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
|
||||
|
||||
if ($this->shouldRotateRefreshTokens()) {
|
||||
// Expire the old refresh token
|
||||
$oldRefreshToken->expire();
|
||||
|
||||
// Generate a new refresh token
|
||||
$newRefreshToken = new RefreshTokenEntity($this->server);
|
||||
$newRefreshToken->setId(SecureKey::generate());
|
||||
$newRefreshToken->setExpireTime($this->getRefreshTokenTTL() + time());
|
||||
$newRefreshToken->setAccessToken($newAccessToken);
|
||||
$newRefreshToken->save();
|
||||
|
||||
$this->server->getTokenType()->setParam('refresh_token', $newRefreshToken->getId());
|
||||
} else {
|
||||
$oldRefreshToken->setAccessToken($newAccessToken);
|
||||
$oldRefreshToken->save();
|
||||
}
|
||||
|
||||
return $this->server->getTokenType()->generateResponse();
|
||||
return [
|
||||
'client_id' => $reader->getClientId(),
|
||||
'refresh_token_id' => '', // This value used only to invalidate old token
|
||||
'access_token_id' => '', // This value used only to invalidate old token
|
||||
'scopes' => $reader->getScopes(),
|
||||
'user_id' => $reader->getAccountId(),
|
||||
'expire_time' => null,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
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 null;
|
||||
}
|
||||
|
||||
}
|
49
api/components/OAuth2/Repositories/AccessTokenRepository.php
Normal file
49
api/components/OAuth2/Repositories/AccessTokenRepository.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
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
|
||||
): AccessTokenEntityInterface {
|
||||
$accessToken = new AccessTokenEntity();
|
||||
$accessToken->setClient($clientEntity);
|
||||
array_map([$accessToken, 'addScope'], $scopes);
|
||||
if ($userIdentifier !== null) {
|
||||
$accessToken->setUserIdentifier($userIdentifier);
|
||||
}
|
||||
|
||||
return $accessToken;
|
||||
}
|
||||
|
||||
public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void {
|
||||
// We don't store access tokens, so there's no need to do anything here
|
||||
}
|
||||
|
||||
public function revokeAccessToken($tokenId): void {
|
||||
// We don't store access tokens, so there's no need to do anything here
|
||||
}
|
||||
|
||||
public function isAccessTokenRevoked($tokenId): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
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;
|
||||
}
|
||||
|
||||
}
|
48
api/components/OAuth2/Repositories/ClientRepository.php
Normal file
48
api/components/OAuth2/Repositories/ClientRepository.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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 ($client->type !== OauthClient::TYPE_APPLICATION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null && $clientSecret !== $client->secret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function findModel(string $id): ?OauthClient {
|
||||
$client = OauthClient::findOne(['id' => $id]);
|
||||
if ($client === null || $client->type !== OauthClient::TYPE_APPLICATION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
}
|
30
api/components/OAuth2/Repositories/EmptyScopeRepository.php
Normal file
30
api/components/OAuth2/Repositories/EmptyScopeRepository.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
|
||||
/**
|
||||
* In our application we use separate scopes repositories for different grants.
|
||||
* To create an instance of the authorization server, you need to pass the scopes
|
||||
* repository. This class acts as a dummy to meet this requirement.
|
||||
*/
|
||||
class EmptyScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $client,
|
||||
$userIdentifier = null
|
||||
): array {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
use api\components\OAuth2\Entities\ScopeEntity;
|
||||
use api\rbac\Permissions as P;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class InternalScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
private const ALLOWED_SCOPES = [
|
||||
P::CHANGE_ACCOUNT_USERNAME,
|
||||
P::CHANGE_ACCOUNT_PASSWORD,
|
||||
P::BLOCK_ACCOUNT,
|
||||
P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
P::ESCAPE_IDENTITY_VERIFICATION,
|
||||
];
|
||||
|
||||
private const PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
'internal_account_info' => P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
];
|
||||
|
||||
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
|
||||
$identifier = $this->convertToInternalPermission($identifier);
|
||||
if (!in_array($identifier, self::ALLOWED_SCOPES, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScopeEntity($identifier);
|
||||
}
|
||||
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $client,
|
||||
$userIdentifier = null
|
||||
): array {
|
||||
/** @var ClientEntity $client */
|
||||
Assert::isInstanceOf($client, ClientEntity::class);
|
||||
|
||||
if (empty($scopes)) {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
// Right now we have no available scopes for the client_credentials grant
|
||||
if (!$client->isTrusted()) {
|
||||
throw OAuthServerException::invalidScope($scopes[0]->getIdentifier());
|
||||
}
|
||||
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
private function convertToInternalPermission(string $publicScope): string {
|
||||
return self::PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS[$publicScope] ?? $publicScope;
|
||||
}
|
||||
|
||||
}
|
55
api/components/OAuth2/Repositories/PublicScopeRepository.php
Normal file
55
api/components/OAuth2/Repositories/PublicScopeRepository.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Repositories;
|
||||
|
||||
use api\components\OAuth2\Entities\ScopeEntity;
|
||||
use api\rbac\Permissions as P;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
|
||||
|
||||
class PublicScopeRepository implements ScopeRepositoryInterface {
|
||||
|
||||
public const OFFLINE_ACCESS = 'offline_access';
|
||||
public const CHANGE_SKIN = 'change_skin';
|
||||
|
||||
private const ACCOUNT_INFO = 'account_info';
|
||||
private const ACCOUNT_EMAIL = 'account_email';
|
||||
|
||||
private const PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
self::ACCOUNT_INFO => P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
self::ACCOUNT_EMAIL => P::OBTAIN_ACCOUNT_EMAIL,
|
||||
];
|
||||
|
||||
private const ALLOWED_SCOPES = [
|
||||
P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
P::OBTAIN_ACCOUNT_EMAIL,
|
||||
P::MINECRAFT_SERVER_SESSION,
|
||||
self::OFFLINE_ACCESS,
|
||||
self::CHANGE_SKIN,
|
||||
];
|
||||
|
||||
public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface {
|
||||
$identifier = $this->convertToInternalPermission($identifier);
|
||||
if (!in_array($identifier, self::ALLOWED_SCOPES, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScopeEntity($identifier);
|
||||
}
|
||||
|
||||
public function finalizeScopes(
|
||||
array $scopes,
|
||||
$grantType,
|
||||
ClientEntityInterface $clientEntity,
|
||||
$userIdentifier = null
|
||||
): array {
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
private function convertToInternalPermission(string $publicScope): string {
|
||||
return self::PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS[$publicScope] ?? $publicScope;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<?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 {
|
||||
|
||||
public function getNewRefreshToken(): ?RefreshTokenEntityInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public function revokeRefreshToken($tokenId): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public function isRefreshTokenRevoked($tokenId): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
12
api/components/OAuth2/ResponseTypes/BearerTokenResponse.php
Normal file
12
api/components/OAuth2/ResponseTypes/BearerTokenResponse.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\ResponseTypes;
|
||||
|
||||
use api\components\OAuth2\CryptTrait;
|
||||
use League\OAuth2\Server\ResponseTypes\BearerTokenResponse as BaseBearerTokenResponse;
|
||||
|
||||
class BearerTokenResponse extends BaseBearerTokenResponse {
|
||||
use CryptTrait;
|
||||
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Storage;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use common\components\Redis\Key;
|
||||
use common\components\Redis\Set;
|
||||
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
|
||||
use League\OAuth2\Server\Entity\ScopeEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\AccessTokenInterface;
|
||||
use yii\helpers\Json;
|
||||
|
||||
class AccessTokenStorage extends AbstractStorage implements AccessTokenInterface {
|
||||
|
||||
public $dataTable = 'oauth_access_tokens';
|
||||
|
||||
public function get($token) {
|
||||
$result = Json::decode((new Key($this->dataTable, $token))->getValue());
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = new AccessTokenEntity($this->server);
|
||||
$token->setId($result['id']);
|
||||
$token->setExpireTime($result['expire_time']);
|
||||
$token->setSessionId($result['session_id']);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function getScopes(OriginalAccessTokenEntity $token) {
|
||||
$scopes = $this->scopes($token->getId());
|
||||
$entities = [];
|
||||
foreach ($scopes as $scope) {
|
||||
if ($this->server->getScopeStorage()->get($scope) !== null) {
|
||||
$entities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
|
||||
}
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
public function create($token, $expireTime, $sessionId) {
|
||||
$payload = Json::encode([
|
||||
'id' => $token,
|
||||
'expire_time' => $expireTime,
|
||||
'session_id' => $sessionId,
|
||||
]);
|
||||
|
||||
$this->key($token)->setValue($payload)->expireAt($expireTime);
|
||||
}
|
||||
|
||||
public function associateScope(OriginalAccessTokenEntity $token, ScopeEntity $scope) {
|
||||
$this->scopes($token->getId())->add($scope->getId())->expireAt($token->getExpireTime());
|
||||
}
|
||||
|
||||
public function delete(OriginalAccessTokenEntity $token) {
|
||||
$this->key($token->getId())->delete();
|
||||
$this->scopes($token->getId())->delete();
|
||||
}
|
||||
|
||||
private function key(string $token): Key {
|
||||
return new Key($this->dataTable, $token);
|
||||
}
|
||||
|
||||
private function scopes(string $token): Set {
|
||||
return new Set($this->dataTable, $token, 'scopes');
|
||||
}
|
||||
|
||||
}
|
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Storage;
|
||||
|
||||
use api\components\OAuth2\Entities\AuthCodeEntity;
|
||||
use common\components\Redis\Key;
|
||||
use common\components\Redis\Set;
|
||||
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
|
||||
use League\OAuth2\Server\Entity\ScopeEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\AuthCodeInterface;
|
||||
use yii\helpers\Json;
|
||||
|
||||
class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface {
|
||||
|
||||
public $dataTable = 'oauth_auth_codes';
|
||||
|
||||
public function get($code) {
|
||||
$result = Json::decode((new Key($this->dataTable, $code))->getValue());
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = new AuthCodeEntity($this->server);
|
||||
$entity->setId($result['id']);
|
||||
$entity->setExpireTime($result['expire_time']);
|
||||
$entity->setSessionId($result['session_id']);
|
||||
$entity->setRedirectUri($result['client_redirect_uri']);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function create($token, $expireTime, $sessionId, $redirectUri) {
|
||||
$payload = Json::encode([
|
||||
'id' => $token,
|
||||
'expire_time' => $expireTime,
|
||||
'session_id' => $sessionId,
|
||||
'client_redirect_uri' => $redirectUri,
|
||||
]);
|
||||
|
||||
$this->key($token)->setValue($payload)->expireAt($expireTime);
|
||||
}
|
||||
|
||||
public function getScopes(OriginalAuthCodeEntity $token) {
|
||||
$scopes = $this->scopes($token->getId());
|
||||
$scopesEntities = [];
|
||||
foreach ($scopes as $scope) {
|
||||
if ($this->server->getScopeStorage()->get($scope) !== null) {
|
||||
$scopesEntities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
|
||||
}
|
||||
}
|
||||
|
||||
return $scopesEntities;
|
||||
}
|
||||
|
||||
public function associateScope(OriginalAuthCodeEntity $token, ScopeEntity $scope) {
|
||||
$this->scopes($token->getId())->add($scope->getId())->expireAt($token->getExpireTime());
|
||||
}
|
||||
|
||||
public function delete(OriginalAuthCodeEntity $token) {
|
||||
$this->key($token->getId())->delete();
|
||||
$this->scopes($token->getId())->delete();
|
||||
}
|
||||
|
||||
private function key(string $token): Key {
|
||||
return new Key($this->dataTable, $token);
|
||||
}
|
||||
|
||||
private function scopes(string $token): Set {
|
||||
return new Set($this->dataTable, $token, 'scopes');
|
||||
}
|
||||
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Storage;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
use api\components\OAuth2\Entities\SessionEntity;
|
||||
use common\models\OauthClient;
|
||||
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\ClientInterface;
|
||||
use yii\helpers\StringHelper;
|
||||
|
||||
class ClientStorage extends AbstractStorage implements ClientInterface {
|
||||
|
||||
private const REDIRECT_STATIC_PAGE = 'static_page';
|
||||
private const REDIRECT_STATIC_PAGE_WITH_CODE = 'static_page_with_code';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function get($clientId, $clientSecret = null, $redirectUri = null, $grantType = null) {
|
||||
$model = $this->findClient($clientId);
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null && $clientSecret !== $model->secret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: should check application type
|
||||
// For "desktop" app type redirect_uri is not required and should be by default set
|
||||
// to the static redirect, but for "site" it's required always.
|
||||
if ($redirectUri !== null) {
|
||||
if (in_array($redirectUri, [self::REDIRECT_STATIC_PAGE, self::REDIRECT_STATIC_PAGE_WITH_CODE], true)) {
|
||||
// I think we should check the type of application here
|
||||
} else {
|
||||
if (!StringHelper::startsWith($redirectUri, $model->redirect_uri, false)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entity = $this->hydrate($model);
|
||||
$entity->setRedirectUri($redirectUri);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getBySession(OriginalSessionEntity $session) {
|
||||
if (!$session instanceof SessionEntity) {
|
||||
throw new \ErrorException('This module assumes that $session typeof ' . SessionEntity::class);
|
||||
}
|
||||
|
||||
$model = $this->findClient($session->getClientId());
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($model);
|
||||
}
|
||||
|
||||
private function hydrate(OauthClient $model): ClientEntity {
|
||||
$entity = new ClientEntity($this->server);
|
||||
$entity->setId($model->id);
|
||||
$entity->setName($model->name);
|
||||
$entity->setSecret($model->secret);
|
||||
$entity->setIsTrusted($model->is_trusted);
|
||||
$entity->setRedirectUri($model->redirect_uri);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
private function findClient(string $clientId): ?OauthClient {
|
||||
return OauthClient::findOne($clientId);
|
||||
}
|
||||
|
||||
}
|
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Storage;
|
||||
|
||||
use api\components\OAuth2\Entities\RefreshTokenEntity;
|
||||
use common\components\Redis\Key;
|
||||
use common\components\Redis\Set;
|
||||
use common\models\OauthSession;
|
||||
use ErrorException;
|
||||
use League\OAuth2\Server\Entity\RefreshTokenEntity as OriginalRefreshTokenEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\RefreshTokenInterface;
|
||||
use Yii;
|
||||
use yii\helpers\Json;
|
||||
|
||||
class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterface {
|
||||
|
||||
public $dataTable = 'oauth_refresh_tokens';
|
||||
|
||||
public function get($token) {
|
||||
$result = Json::decode((new Key($this->dataTable, $token))->getValue());
|
||||
if ($result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = new RefreshTokenEntity($this->server);
|
||||
$entity->setId($result['id']);
|
||||
$entity->setAccessTokenId($result['access_token_id']);
|
||||
$entity->setSessionId($result['session_id']);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function create($token, $expireTime, $accessToken) {
|
||||
$sessionId = $this->server->getAccessTokenStorage()->get($accessToken)->getSession()->getId();
|
||||
$payload = Json::encode([
|
||||
'id' => $token,
|
||||
'access_token_id' => $accessToken,
|
||||
'session_id' => $sessionId,
|
||||
]);
|
||||
|
||||
$this->key($token)->setValue($payload);
|
||||
$this->sessionHash($sessionId)->add($token);
|
||||
}
|
||||
|
||||
public function delete(OriginalRefreshTokenEntity $token) {
|
||||
if (!$token instanceof RefreshTokenEntity) {
|
||||
throw new ErrorException('Token must be instance of ' . RefreshTokenEntity::class);
|
||||
}
|
||||
|
||||
$this->key($token->getId())->delete();
|
||||
$this->sessionHash($token->getSessionId())->remove($token->getId());
|
||||
}
|
||||
|
||||
public function sessionHash(string $sessionId): Set {
|
||||
$tableName = Yii::$app->db->getSchema()->getRawTableName(OauthSession::tableName());
|
||||
return new Set($tableName, $sessionId, 'refresh_tokens');
|
||||
}
|
||||
|
||||
private function key(string $token): Key {
|
||||
return new Key($this->dataTable, $token);
|
||||
}
|
||||
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Storage;
|
||||
|
||||
use api\components\OAuth2\Entities\ClientEntity;
|
||||
use api\components\OAuth2\Entities\ScopeEntity;
|
||||
use api\rbac\Permissions as P;
|
||||
use Assert\Assert;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\ScopeInterface;
|
||||
|
||||
class ScopeStorage extends AbstractStorage implements ScopeInterface {
|
||||
|
||||
public const OFFLINE_ACCESS = 'offline_access';
|
||||
|
||||
public const CHANGE_SKIN = 'change_skin';
|
||||
|
||||
private const PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
|
||||
'account_info' => P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
'account_email' => P::OBTAIN_ACCOUNT_EMAIL,
|
||||
'account_block' => P::BLOCK_ACCOUNT,
|
||||
'internal_account_info' => P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
];
|
||||
|
||||
private const AUTHORIZATION_CODE_PERMISSIONS = [
|
||||
P::OBTAIN_OWN_ACCOUNT_INFO,
|
||||
P::OBTAIN_ACCOUNT_EMAIL,
|
||||
P::MINECRAFT_SERVER_SESSION,
|
||||
self::OFFLINE_ACCESS,
|
||||
self::CHANGE_SKIN,
|
||||
];
|
||||
|
||||
private const CLIENT_CREDENTIALS_PERMISSIONS = [
|
||||
];
|
||||
|
||||
private const CLIENT_CREDENTIALS_PERMISSIONS_INTERNAL = [
|
||||
P::CHANGE_ACCOUNT_USERNAME,
|
||||
P::CHANGE_ACCOUNT_PASSWORD,
|
||||
P::BLOCK_ACCOUNT,
|
||||
P::OBTAIN_EXTENDED_ACCOUNT_INFO,
|
||||
P::ESCAPE_IDENTITY_VERIFICATION,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $scope
|
||||
* @param string $grantType is passed on if called from the grant.
|
||||
* In this case, you only need to filter out the rights that you can get on this grant.
|
||||
* @param string $clientId
|
||||
*
|
||||
* @return ScopeEntity|null
|
||||
*/
|
||||
public function get($scope, $grantType = null, $clientId = null): ?ScopeEntity {
|
||||
$permission = $this->convertToInternalPermission($scope);
|
||||
|
||||
if ($grantType === 'authorization_code') {
|
||||
$permissions = self::AUTHORIZATION_CODE_PERMISSIONS;
|
||||
} elseif ($grantType === 'client_credentials') {
|
||||
$permissions = self::CLIENT_CREDENTIALS_PERMISSIONS;
|
||||
$isTrusted = false;
|
||||
if ($clientId !== null) {
|
||||
/** @var ClientEntity $client */
|
||||
$client = $this->server->getClientStorage()->get($clientId);
|
||||
Assert::that($client)->isInstanceOf(ClientEntity::class);
|
||||
|
||||
/** @noinspection NullPointerExceptionInspection */
|
||||
$isTrusted = $client->isTrusted();
|
||||
}
|
||||
|
||||
if ($isTrusted) {
|
||||
$permissions = array_merge($permissions, self::CLIENT_CREDENTIALS_PERMISSIONS_INTERNAL);
|
||||
}
|
||||
} else {
|
||||
$permissions = array_merge(
|
||||
self::AUTHORIZATION_CODE_PERMISSIONS,
|
||||
self::CLIENT_CREDENTIALS_PERMISSIONS,
|
||||
self::CLIENT_CREDENTIALS_PERMISSIONS_INTERNAL
|
||||
);
|
||||
}
|
||||
|
||||
if (!in_array($permission, $permissions, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = new ScopeEntity($this->server);
|
||||
$entity->setId($permission);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
private function convertToInternalPermission(string $publicScope): string {
|
||||
return self::PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS[$publicScope] ?? $publicScope;
|
||||
}
|
||||
|
||||
}
|
@@ -1,108 +0,0 @@
|
||||
<?php
|
||||
namespace api\components\OAuth2\Storage;
|
||||
|
||||
use api\components\OAuth2\Entities\AuthCodeEntity;
|
||||
use api\components\OAuth2\Entities\SessionEntity;
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use common\models\OauthSession;
|
||||
use ErrorException;
|
||||
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
|
||||
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
|
||||
use League\OAuth2\Server\Entity\ScopeEntity;
|
||||
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\SessionInterface;
|
||||
use yii\db\Exception;
|
||||
|
||||
class SessionStorage extends AbstractStorage implements SessionInterface {
|
||||
|
||||
/**
|
||||
* @param string $sessionId
|
||||
* @return SessionEntity|null
|
||||
*/
|
||||
public function getById($sessionId): ?SessionEntity {
|
||||
$session = $this->getSessionModel($sessionId);
|
||||
if ($session === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($session);
|
||||
}
|
||||
|
||||
public function getByAccessToken(OriginalAccessTokenEntity $accessToken) {
|
||||
throw new ErrorException('This method is not implemented and should not be used');
|
||||
}
|
||||
|
||||
public function getByAuthCode(OriginalAuthCodeEntity $authCode) {
|
||||
if (!$authCode instanceof AuthCodeEntity) {
|
||||
throw new ErrorException('This module assumes that $authCode typeof ' . AuthCodeEntity::class);
|
||||
}
|
||||
|
||||
return $this->getById($authCode->getSessionId());
|
||||
}
|
||||
|
||||
public function getScopes(OriginalSessionEntity $entity) {
|
||||
$session = $this->getSessionModel($entity->getId());
|
||||
if ($session === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($session->getScopes() as $scope) {
|
||||
if ($this->server->getScopeStorage()->get($scope) !== null) {
|
||||
$result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function create($ownerType, $ownerId, $clientId, $clientRedirectUri = null) {
|
||||
$sessionId = OauthSession::find()
|
||||
->select('id')
|
||||
->andWhere([
|
||||
'client_id' => $clientId,
|
||||
'owner_type' => $ownerType,
|
||||
'owner_id' => (string)$ownerId, // Casts as a string to make the indexes work, because the varchar field
|
||||
])->scalar();
|
||||
|
||||
if ($sessionId === false) {
|
||||
$model = new OauthSession();
|
||||
$model->client_id = $clientId;
|
||||
$model->owner_type = $ownerType;
|
||||
$model->owner_id = $ownerId;
|
||||
$model->client_redirect_uri = $clientRedirectUri;
|
||||
|
||||
if (!$model->save()) {
|
||||
throw new Exception('Cannot save ' . OauthSession::class . ' model.');
|
||||
}
|
||||
|
||||
$sessionId = $model->id;
|
||||
}
|
||||
|
||||
return $sessionId;
|
||||
}
|
||||
|
||||
public function associateScope(OriginalSessionEntity $sessionEntity, ScopeEntity $scopeEntity): void {
|
||||
$session = $this->getSessionModel($sessionEntity->getId());
|
||||
if ($session === null) {
|
||||
throw new ThisShouldNotHappenException('Cannot find oauth session');
|
||||
}
|
||||
|
||||
$session->getScopes()->add($scopeEntity->getId());
|
||||
}
|
||||
|
||||
private function getSessionModel(string $sessionId): ?OauthSession {
|
||||
return OauthSession::findOne(['id' => $sessionId]);
|
||||
}
|
||||
|
||||
private function hydrate(OauthSession $sessionModel): SessionEntity {
|
||||
$entity = new SessionEntity($this->server);
|
||||
$entity->setId($sessionModel->id);
|
||||
$entity->setClientId($sessionModel->client_id);
|
||||
$entity->setOwner($sessionModel->owner_type, $sessionModel->owner_id);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\OAuth2\Utils;
|
||||
|
||||
class Scopes {
|
||||
|
||||
/**
|
||||
* In the earlier versions of Accounts Ely.by backend we had a comma-separated scopes
|
||||
* list, while by OAuth2 standard it they should be separated by a space. Shit happens :)
|
||||
* So override scopes validation function to reformat passed value.
|
||||
*
|
||||
* @param string|array $scopes
|
||||
* @return string
|
||||
*/
|
||||
public static function format($scopes): string {
|
||||
if ($scopes === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (is_array($scopes)) {
|
||||
return implode(' ', $scopes);
|
||||
}
|
||||
|
||||
return str_replace(',', ' ', $scopes);
|
||||
}
|
||||
|
||||
}
|
@@ -8,6 +8,7 @@ use Exception;
|
||||
use Lcobucci\JWT\Builder;
|
||||
use Lcobucci\JWT\Parser;
|
||||
use Lcobucci\JWT\Token;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Webmozart\Assert\Assert;
|
||||
use yii\base\Component as BaseComponent;
|
||||
|
||||
@@ -17,6 +18,10 @@ class Component extends BaseComponent {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @deprecated In earlier versions of the application, JWT were signed by a synchronous encryption algorithm.
|
||||
* Now asynchronous encryption is used instead, and this logic is saved for a transitional period.
|
||||
* I think it can be safely removed, but I'll not do it yet, because at the time of writing the comment
|
||||
* there were enough changes in the code already.
|
||||
*/
|
||||
public $hmacKey;
|
||||
|
||||
@@ -35,6 +40,11 @@ class Component extends BaseComponent {
|
||||
*/
|
||||
public $privateKeyPass;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $encryptionKey;
|
||||
|
||||
/**
|
||||
* @var AlgorithmsManager|null
|
||||
*/
|
||||
@@ -45,19 +55,22 @@ class Component extends BaseComponent {
|
||||
Assert::notEmpty($this->hmacKey, 'hmacKey must be set');
|
||||
Assert::notEmpty($this->privateKeyPath, 'privateKeyPath must be set');
|
||||
Assert::notEmpty($this->publicKeyPath, 'publicKeyPath must be set');
|
||||
Assert::notEmpty($this->encryptionKey, 'encryptionKey must be set');
|
||||
}
|
||||
|
||||
public function create(array $payloads = [], array $headers = []): Token {
|
||||
$now = Carbon::now();
|
||||
$builder = (new Builder())
|
||||
->issuedAt($now->getTimestamp())
|
||||
->expiresAt($now->addHour()->getTimestamp());
|
||||
$builder = (new Builder())->issuedAt($now->getTimestamp());
|
||||
if (isset($payloads['exp'])) {
|
||||
$builder->expiresAt($payloads['exp']);
|
||||
}
|
||||
|
||||
foreach ($payloads as $claim => $value) {
|
||||
$builder->withClaim($claim, $value);
|
||||
$builder->withClaim($claim, $this->prepareValue($value));
|
||||
}
|
||||
|
||||
foreach ($headers as $claim => $value) {
|
||||
$builder->withHeader($claim, $value);
|
||||
$builder->withHeader($claim, $this->prepareValue($value));
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
@@ -85,6 +98,28 @@ class Component extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public function encryptValue(string $rawValue): string {
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||
$cipher = Base64UrlSafe::encodeUnpadded($nonce . sodium_crypto_secretbox($rawValue, $nonce, $this->encryptionKey));
|
||||
sodium_memzero($rawValue);
|
||||
|
||||
return $cipher;
|
||||
}
|
||||
|
||||
public function decryptValue(string $encryptedValue): string {
|
||||
$decoded = Base64UrlSafe::decode($encryptedValue);
|
||||
Assert::true(mb_strlen($decoded, '8bit') >= (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES));
|
||||
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
|
||||
$cipherText = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
|
||||
|
||||
$rawValue = sodium_crypto_secretbox_open($cipherText, $nonce, $this->encryptionKey);
|
||||
Assert::true($rawValue !== false);
|
||||
sodium_memzero($cipherText);
|
||||
|
||||
return $rawValue;
|
||||
}
|
||||
|
||||
private function getAlgorithmManager(): AlgorithmsManager {
|
||||
if ($this->algorithmManager === null) {
|
||||
$this->algorithmManager = new AlgorithmsManager([
|
||||
@@ -100,4 +135,12 @@ class Component extends BaseComponent {
|
||||
return $this->algorithmManager;
|
||||
}
|
||||
|
||||
private function prepareValue($value) {
|
||||
if ($value instanceof EncryptedValue) {
|
||||
return $this->encryptValue($value->getValue());
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
21
api/components/Tokens/EncryptedValue.php
Normal file
21
api/components/Tokens/EncryptedValue.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\Tokens;
|
||||
|
||||
class EncryptedValue {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
public function __construct(string $value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValue(): string {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
}
|
64
api/components/Tokens/TokenReader.php
Normal file
64
api/components/Tokens/TokenReader.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\Tokens;
|
||||
|
||||
use Lcobucci\JWT\Token;
|
||||
use Yii;
|
||||
|
||||
class TokenReader {
|
||||
|
||||
/**
|
||||
* @var Token
|
||||
*/
|
||||
private $token;
|
||||
|
||||
public function __construct(Token $token) {
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function getAccountId(): ?int {
|
||||
$sub = $this->token->getClaim('sub', false);
|
||||
if ($sub === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mb_strpos((string)$sub, TokensFactory::SUB_ACCOUNT_PREFIX) !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)mb_substr($sub, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX));
|
||||
}
|
||||
|
||||
public function getClientId(): ?string {
|
||||
$aud = $this->token->getClaim('aud', false);
|
||||
if ($aud === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mb_strpos((string)$aud, TokensFactory::AUD_CLIENT_PREFIX) !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mb_substr($aud, mb_strlen(TokensFactory::AUD_CLIENT_PREFIX));
|
||||
}
|
||||
|
||||
public function getScopes(): ?array {
|
||||
$scopes = $this->token->getClaim('ely-scopes', false);
|
||||
if ($scopes === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return explode(',', $scopes);
|
||||
}
|
||||
|
||||
public function getMinecraftClientToken(): ?string {
|
||||
$encodedClientToken = $this->token->getClaim('ely-client-token', false);
|
||||
if ($encodedClientToken === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Yii::$app->tokens->decryptValue($encodedClientToken);
|
||||
}
|
||||
|
||||
}
|
@@ -3,20 +3,28 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\components\Tokens;
|
||||
|
||||
use api\rbac\Permissions as P;
|
||||
use api\rbac\Roles as R;
|
||||
use Carbon\Carbon;
|
||||
use common\models\Account;
|
||||
use common\models\AccountSession;
|
||||
use DateTime;
|
||||
use Lcobucci\JWT\Token;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use Yii;
|
||||
use yii\base\Component;
|
||||
|
||||
class TokensFactory {
|
||||
class TokensFactory extends Component {
|
||||
|
||||
public const SUB_ACCOUNT_PREFIX = 'ely|';
|
||||
public const AUD_CLIENT_PREFIX = 'client|';
|
||||
|
||||
public static function createForAccount(Account $account, AccountSession $session = null): Token {
|
||||
public function createForWebAccount(Account $account, AccountSession $session = null): Token {
|
||||
$payloads = [
|
||||
'ely-scopes' => 'accounts_web_user',
|
||||
'sub' => self::SUB_ACCOUNT_PREFIX . $account->id,
|
||||
'ely-scopes' => $this->prepareScopes([R::ACCOUNTS_WEB_USER]),
|
||||
'sub' => $this->buildSub($account->id),
|
||||
'exp' => Carbon::now()->addHour()->getTimestamp(),
|
||||
];
|
||||
if ($session === null) {
|
||||
// If we don't remember a session, the token should live longer
|
||||
@@ -29,4 +37,52 @@ class TokensFactory {
|
||||
return Yii::$app->tokens->create($payloads);
|
||||
}
|
||||
|
||||
public function createForOAuthClient(AccessTokenEntityInterface $accessToken): Token {
|
||||
$payloads = [
|
||||
'aud' => $this->buildAud($accessToken->getClient()->getIdentifier()),
|
||||
'ely-scopes' => $this->prepareScopes($accessToken->getScopes()),
|
||||
];
|
||||
if ($accessToken->getExpiryDateTime() > new DateTime()) {
|
||||
$payloads['exp'] = $accessToken->getExpiryDateTime()->getTimestamp();
|
||||
}
|
||||
|
||||
if ($accessToken->getUserIdentifier() !== null) {
|
||||
$payloads['sub'] = $this->buildSub($accessToken->getUserIdentifier());
|
||||
}
|
||||
|
||||
return Yii::$app->tokens->create($payloads);
|
||||
}
|
||||
|
||||
public function createForMinecraftAccount(Account $account, string $clientToken): Token {
|
||||
return Yii::$app->tokens->create([
|
||||
'ely-scopes' => $this->prepareScopes([P::MINECRAFT_SERVER_SESSION]),
|
||||
'ely-client-token' => new EncryptedValue($clientToken),
|
||||
'sub' => $this->buildSub($account->id),
|
||||
'exp' => Carbon::now()->addDays(2)->getTimestamp(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ScopeEntityInterface[]|string[] $scopes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function prepareScopes(array $scopes): string {
|
||||
return implode(',', array_map(function($scope): string { // TODO: replace to the space if it's possible
|
||||
if ($scope instanceof ScopeEntityInterface) {
|
||||
return $scope->getIdentifier();
|
||||
}
|
||||
|
||||
return $scope;
|
||||
}, $scopes));
|
||||
}
|
||||
|
||||
private function buildSub(int $accountId): string {
|
||||
return self::SUB_ACCOUNT_PREFIX . $accountId;
|
||||
}
|
||||
|
||||
private function buildAud(string $clientId): string {
|
||||
return self::AUD_CLIENT_PREFIX . $clientId;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ namespace api\components\User;
|
||||
|
||||
use common\models\Account;
|
||||
use common\models\AccountSession;
|
||||
use common\models\OauthClient;
|
||||
use Webmozart\Assert\Assert;
|
||||
use yii\web\User as YiiUserComponent;
|
||||
|
||||
/**
|
||||
@@ -78,6 +80,15 @@ class Component extends YiiUserComponent {
|
||||
}
|
||||
|
||||
if (!($mode & self::KEEP_MINECRAFT_SESSIONS)) {
|
||||
/** @var \common\models\OauthSession|null $minecraftSession */
|
||||
$minecraftSession = $account->getOauthSessions()
|
||||
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])
|
||||
->one();
|
||||
if ($minecraftSession !== null) {
|
||||
$minecraftSession->revoked_at = time();
|
||||
Assert::true($minecraftSession->save());
|
||||
}
|
||||
|
||||
foreach ($account->minecraftAccessKeys as $minecraftAccessKey) {
|
||||
$minecraftAccessKey->delete();
|
||||
}
|
||||
|
@@ -8,19 +8,24 @@ use yii\web\UnauthorizedHttpException;
|
||||
class IdentityFactory {
|
||||
|
||||
/**
|
||||
* @throws UnauthorizedHttpException
|
||||
* @param string $token
|
||||
* @param string $type
|
||||
*
|
||||
* @return IdentityInterface
|
||||
* @throws UnauthorizedHttpException
|
||||
*/
|
||||
public static function findIdentityByAccessToken($token, $type = null): IdentityInterface {
|
||||
if (empty($token)) {
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
if (!empty($token)) {
|
||||
if (mb_strlen($token) === 40) {
|
||||
return LegacyOAuth2Identity::findIdentityByAccessToken($token, $type);
|
||||
}
|
||||
|
||||
if (substr_count($token, '.') === 2) {
|
||||
return JwtIdentity::findIdentityByAccessToken($token, $type);
|
||||
}
|
||||
}
|
||||
|
||||
if (substr_count($token, '.') === 2) {
|
||||
return JwtIdentity::findIdentityByAccessToken($token, $type);
|
||||
}
|
||||
|
||||
return OAuth2Identity::findIdentityByAccessToken($token, $type);
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,13 +3,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\components\User;
|
||||
|
||||
use api\components\Tokens\TokensFactory;
|
||||
use api\components\Tokens\TokenReader;
|
||||
use Carbon\Carbon;
|
||||
use common\models\Account;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
use Exception;
|
||||
use Lcobucci\JWT\Token;
|
||||
use Lcobucci\JWT\ValidationData;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
@@ -21,6 +22,11 @@ class JwtIdentity implements IdentityInterface {
|
||||
*/
|
||||
private $token;
|
||||
|
||||
/**
|
||||
* @var TokenReader|null
|
||||
*/
|
||||
private $reader;
|
||||
|
||||
private function __construct(Token $token) {
|
||||
$this->token = $token;
|
||||
}
|
||||
@@ -46,9 +52,21 @@ class JwtIdentity implements IdentityInterface {
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
}
|
||||
|
||||
$sub = $token->getClaim('sub', false);
|
||||
if ($sub !== false && strpos((string)$sub, TokensFactory::SUB_ACCOUNT_PREFIX) !== 0) {
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
$tokenReader = new TokenReader($token);
|
||||
$accountId = $tokenReader->getAccountId();
|
||||
if ($accountId !== null) {
|
||||
$iat = $token->getClaim('iat');
|
||||
if ($tokenReader->getMinecraftClientToken() !== null
|
||||
&& self::isRevoked($accountId, OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER, $iat)
|
||||
) {
|
||||
throw new UnauthorizedHttpException('Token has been revoked');
|
||||
}
|
||||
|
||||
if ($tokenReader->getClientId() !== null
|
||||
&& self::isRevoked($accountId, $tokenReader->getClientId(), $iat)
|
||||
) {
|
||||
throw new UnauthorizedHttpException('Token has been revoked');
|
||||
}
|
||||
}
|
||||
|
||||
return new self($token);
|
||||
@@ -59,24 +77,11 @@ class JwtIdentity implements IdentityInterface {
|
||||
}
|
||||
|
||||
public function getAccount(): ?Account {
|
||||
$subject = $this->token->getClaim('sub', false);
|
||||
if ($subject === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Assert::startsWith($subject, TokensFactory::SUB_ACCOUNT_PREFIX);
|
||||
$accountId = (int)mb_substr($subject, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX));
|
||||
|
||||
return Account::findOne(['id' => $accountId]);
|
||||
return Account::findOne(['id' => $this->getReader()->getAccountId()]);
|
||||
}
|
||||
|
||||
public function getAssignedPermissions(): array {
|
||||
$scopesClaim = $this->token->getClaim('ely-scopes', false);
|
||||
if ($scopesClaim === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return explode(',', $scopesClaim);
|
||||
return $this->getReader()->getScopes() ?? [];
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
@@ -96,6 +101,19 @@ class JwtIdentity implements IdentityInterface {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
private static function isRevoked(int $accountId, string $clientId, int $iat): bool {
|
||||
$session = OauthSession::findOne(['account_id' => $accountId, 'client_id' => $clientId]);
|
||||
return $session !== null && $session->revoked_at !== null && $session->revoked_at > $iat;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
private function getReader(): TokenReader {
|
||||
if ($this->reader === null) {
|
||||
$this->reader = new TokenReader($this->token);
|
||||
}
|
||||
|
||||
return $this->reader;
|
||||
}
|
||||
|
||||
}
|
||||
|
119
api/components/User/LegacyOAuth2Identity.php
Normal file
119
api/components/User/LegacyOAuth2Identity.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\User;
|
||||
|
||||
use common\models\Account;
|
||||
use common\models\OauthSession;
|
||||
use Exception;
|
||||
use Yii;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class LegacyOAuth2Identity implements IdentityInterface {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $accessToken;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sessionId;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $scopes;
|
||||
|
||||
/**
|
||||
* @var OauthSession|null
|
||||
*/
|
||||
private $session = false;
|
||||
|
||||
private function __construct(string $accessToken, int $sessionId, array $scopes) {
|
||||
$this->accessToken = $accessToken;
|
||||
$this->sessionId = $sessionId;
|
||||
$this->scopes = $scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws UnauthorizedHttpException
|
||||
* @return IdentityInterface
|
||||
*/
|
||||
public static function findIdentityByAccessToken($token, $type = null): IdentityInterface {
|
||||
$tokenParams = self::findRecordOnLegacyStorage($token);
|
||||
if ($tokenParams === null) {
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
}
|
||||
|
||||
if ($tokenParams['expire_time'] < time()) {
|
||||
throw new UnauthorizedHttpException('Token expired');
|
||||
}
|
||||
|
||||
return new static($token, $tokenParams['session_id'], $tokenParams['scopes']);
|
||||
}
|
||||
|
||||
public function getAccount(): ?Account {
|
||||
$session = $this->getSession();
|
||||
if ($session === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $session->account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAssignedPermissions(): array {
|
||||
return $this->scopes;
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
public function getAuthKey() {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
public function validateAuthKey($authKey) {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
public static function findIdentity($id) {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
private static function findRecordOnLegacyStorage(string $accessToken): ?array {
|
||||
$record = Yii::$app->redis->get("oauth:access:tokens:{$accessToken}");
|
||||
if ($record === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode($record, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data['scopes'] = (array)Yii::$app->redis->smembers("oauth:access:tokens:{$accessToken}:scopes");
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getSession(): ?OauthSession {
|
||||
if ($this->session === false) {
|
||||
$this->session = OauthSession::findOne(['id' => $this->sessionId]);
|
||||
}
|
||||
|
||||
return $this->session;
|
||||
}
|
||||
|
||||
}
|
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\components\User;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use common\models\Account;
|
||||
use common\models\OauthSession;
|
||||
use Yii;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class OAuth2Identity implements IdentityInterface {
|
||||
|
||||
/**
|
||||
* @var AccessTokenEntity
|
||||
*/
|
||||
private $_accessToken;
|
||||
|
||||
private function __construct(AccessTokenEntity $accessToken) {
|
||||
$this->_accessToken = $accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws UnauthorizedHttpException
|
||||
* @return IdentityInterface
|
||||
*/
|
||||
public static function findIdentityByAccessToken($token, $type = null): IdentityInterface {
|
||||
/** @var AccessTokenEntity|null $model */
|
||||
$model = Yii::$app->oauth->getAccessTokenStorage()->get($token);
|
||||
if ($model === null) {
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
}
|
||||
|
||||
if ($model->isExpired()) {
|
||||
throw new UnauthorizedHttpException('Token expired');
|
||||
}
|
||||
|
||||
return new static($model);
|
||||
}
|
||||
|
||||
public function getAccount(): ?Account {
|
||||
$session = $this->getSession();
|
||||
if ($session === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getSession()->account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAssignedPermissions(): array {
|
||||
return array_keys($this->_accessToken->getScopes());
|
||||
}
|
||||
|
||||
public function getId(): string {
|
||||
return $this->_accessToken->getId();
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
public function getAuthKey() {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
public function validateAuthKey($authKey) {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
public static function findIdentity($id) {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
private function getSession(): ?OauthSession {
|
||||
return OauthSession::findOne(['id' => $this->_accessToken->getSessionId()]);
|
||||
}
|
||||
|
||||
}
|
@@ -6,6 +6,7 @@ return [
|
||||
'privateKeyPath' => codecept_data_dir('certs/private.pem'),
|
||||
'privateKeyPass' => null,
|
||||
'publicKeyPath' => codecept_data_dir('certs/public.pem'),
|
||||
'encryptionKey' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
],
|
||||
'reCaptcha' => [
|
||||
'public' => 'public-key',
|
||||
|
@@ -7,16 +7,31 @@ return [
|
||||
'params' => [
|
||||
'authserverHost' => getenv('AUTHSERVER_HOST') ?: 'authserver.ely.by',
|
||||
],
|
||||
'modules' => [
|
||||
'authserver' => api\modules\authserver\Module::class,
|
||||
'session' => api\modules\session\Module::class,
|
||||
'mojang' => api\modules\mojang\Module::class,
|
||||
'internal' => api\modules\internal\Module::class,
|
||||
'accounts' => api\modules\accounts\Module::class,
|
||||
'oauth' => api\modules\oauth\Module::class,
|
||||
],
|
||||
'components' => [
|
||||
'user' => [
|
||||
'class' => api\components\User\Component::class,
|
||||
],
|
||||
'oauth' => [
|
||||
'class' => api\components\OAuth2\Component::class,
|
||||
],
|
||||
'tokens' => [
|
||||
'class' => api\components\Tokens\Component::class,
|
||||
'hmacKey' => getenv('JWT_USER_SECRET'),
|
||||
'privateKeyPath' => getenv('JWT_PRIVATE_KEY_PATH') ?: __DIR__ . '/../../data/certs/private.pem',
|
||||
'privateKeyPass' => getenv('JWT_PRIVATE_KEY_PASS') ?: null,
|
||||
'publicKeyPath' => getenv('JWT_PUBLIC_KEY_PATH') ?: __DIR__ . '/../../data/certs/public.pem',
|
||||
'encryptionKey' => getenv('JWT_ENCRYPTION_KEY'),
|
||||
],
|
||||
'tokensFactory' => [
|
||||
'class' => api\components\Tokens\TokensFactory::class,
|
||||
],
|
||||
'log' => [
|
||||
'traceLevel' => YII_DEBUG ? 3 : 0,
|
||||
@@ -83,12 +98,4 @@ return [
|
||||
'class' => api\components\ErrorHandler::class,
|
||||
],
|
||||
],
|
||||
'modules' => [
|
||||
'authserver' => api\modules\authserver\Module::class,
|
||||
'session' => api\modules\session\Module::class,
|
||||
'mojang' => api\modules\mojang\Module::class,
|
||||
'internal' => api\modules\internal\Module::class,
|
||||
'accounts' => api\modules\accounts\Module::class,
|
||||
'oauth' => api\modules\oauth\Module::class,
|
||||
],
|
||||
];
|
||||
|
@@ -7,6 +7,8 @@ namespace api\exceptions;
|
||||
* The exception can be used for cases where the outcome doesn't seem to be expected,
|
||||
* but can theoretically happen. The goal is to capture these areas and refine the logic
|
||||
* if such situations do occur.
|
||||
*
|
||||
* @deprecated use \Webmozart\Assert\Assert to ensure, that action has been successfully performed
|
||||
*/
|
||||
class ThisShouldNotHappenException extends Exception {
|
||||
|
||||
|
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\aop\annotations\CollectModelMetrics;
|
||||
use api\components\Tokens\TokensFactory;
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\EmailActivationKeyValidator;
|
||||
use common\models\Account;
|
||||
@@ -48,7 +47,7 @@ class ConfirmEmailForm extends ApiForm {
|
||||
$session->generateRefreshToken();
|
||||
Assert::true($session->save(), 'Cannot save account session model');
|
||||
|
||||
$token = TokensFactory::createForAccount($account, $session);
|
||||
$token = Yii::$app->tokensFactory->createForWebAccount($account, $session);
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
|
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\aop\annotations\CollectModelMetrics;
|
||||
use api\components\Tokens\TokensFactory;
|
||||
use api\models\base\ApiForm;
|
||||
use api\traits\AccountFinder;
|
||||
use api\validators\TotpValidator;
|
||||
@@ -121,7 +120,7 @@ class LoginForm extends ApiForm {
|
||||
Assert::true($session->save(), 'Cannot save account session model');
|
||||
}
|
||||
|
||||
$token = TokensFactory::createForAccount($account, $session);
|
||||
$token = Yii::$app->tokensFactory->createForWebAccount($account, $session);
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
|
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\aop\annotations\CollectModelMetrics;
|
||||
use api\components\Tokens\TokensFactory;
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\EmailActivationKeyValidator;
|
||||
use common\helpers\Error as E;
|
||||
@@ -56,7 +55,7 @@ class RecoverPasswordForm extends ApiForm {
|
||||
|
||||
Assert::true($account->save(), 'Unable activate user account.');
|
||||
|
||||
$token = TokensFactory::createForAccount($account);
|
||||
$token = Yii::$app->tokensFactory->createForWebAccount($account);
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
|
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\aop\annotations\CollectModelMetrics;
|
||||
use api\components\Tokens\TokensFactory;
|
||||
use api\models\base\ApiForm;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\AccountSession;
|
||||
@@ -47,7 +46,7 @@ class RefreshTokenForm extends ApiForm {
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
$token = TokensFactory::createForAccount($account, $session);
|
||||
$token = Yii::$app->tokensFactory->createForWebAccount($account, $session);
|
||||
|
||||
$session->setIp(Yii::$app->request->userIP);
|
||||
$session->touch('last_refreshed_at');
|
||||
|
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\models\base;
|
||||
|
||||
use yii\base\Model;
|
||||
|
||||
class ApiForm extends Model {
|
||||
|
||||
public function formName() {
|
||||
public function formName(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\controllers;
|
||||
|
||||
use api\controllers\Controller;
|
||||
@@ -14,7 +16,7 @@ class AuthenticationController extends Controller {
|
||||
return $behaviors;
|
||||
}
|
||||
|
||||
public function verbs() {
|
||||
public function verbs(): array {
|
||||
return [
|
||||
'authenticate' => ['POST'],
|
||||
'refresh' => ['POST'],
|
||||
@@ -24,21 +26,35 @@ class AuthenticationController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
public function actionAuthenticate() {
|
||||
/**
|
||||
* @return array
|
||||
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function actionAuthenticate(): array {
|
||||
$model = new models\AuthenticationForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
|
||||
return $model->authenticate()->getResponseData(true);
|
||||
}
|
||||
|
||||
public function actionRefresh() {
|
||||
/**
|
||||
* @return array
|
||||
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function actionRefresh(): array {
|
||||
$model = new models\RefreshTokenForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
|
||||
return $model->refresh()->getResponseData(false);
|
||||
}
|
||||
|
||||
public function actionValidate() {
|
||||
/**
|
||||
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function actionValidate(): void {
|
||||
$model = new models\ValidateForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
$model->validateToken();
|
||||
@@ -46,7 +62,11 @@ class AuthenticationController extends Controller {
|
||||
// In case of an error, an exception is thrown which will be processed by ErrorHandler
|
||||
}
|
||||
|
||||
public function actionSignout() {
|
||||
/**
|
||||
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function actionSignout(): void {
|
||||
$model = new models\SignoutForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
$model->signout();
|
||||
@@ -54,7 +74,10 @@ class AuthenticationController extends Controller {
|
||||
// In case of an error, an exception is thrown which will be processed by ErrorHandler
|
||||
}
|
||||
|
||||
public function actionInvalidate() {
|
||||
/**
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function actionInvalidate(): void {
|
||||
$model = new models\InvalidateForm();
|
||||
$model->load(Yii::$app->request->post());
|
||||
$model->invalidateToken();
|
||||
|
@@ -1,39 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use common\models\MinecraftAccessKey;
|
||||
use common\models\Account;
|
||||
use Lcobucci\JWT\Token;
|
||||
|
||||
class AuthenticateData {
|
||||
|
||||
/**
|
||||
* @var MinecraftAccessKey
|
||||
* @var Account
|
||||
*/
|
||||
private $minecraftAccessKey;
|
||||
private $account;
|
||||
|
||||
public function __construct(MinecraftAccessKey $minecraftAccessKey) {
|
||||
$this->minecraftAccessKey = $minecraftAccessKey;
|
||||
}
|
||||
/**
|
||||
* @var Token
|
||||
*/
|
||||
private $accessToken;
|
||||
|
||||
public function getMinecraftAccessKey(): MinecraftAccessKey {
|
||||
return $this->minecraftAccessKey;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $clientToken;
|
||||
|
||||
public function __construct(Account $account, string $accessToken, string $clientToken) {
|
||||
$this->account = $account;
|
||||
$this->accessToken = $accessToken;
|
||||
$this->clientToken = $clientToken;
|
||||
}
|
||||
|
||||
public function getResponseData(bool $includeAvailableProfiles = false): array {
|
||||
$accessKey = $this->minecraftAccessKey;
|
||||
$account = $accessKey->account;
|
||||
|
||||
$result = [
|
||||
'accessToken' => $accessKey->access_token,
|
||||
'clientToken' => $accessKey->client_token,
|
||||
'accessToken' => $this->accessToken,
|
||||
'clientToken' => $this->clientToken,
|
||||
'selectedProfile' => [
|
||||
'id' => $account->uuid,
|
||||
'name' => $account->username,
|
||||
'id' => $this->account->uuid,
|
||||
'name' => $this->account->username,
|
||||
'legacy' => false,
|
||||
],
|
||||
];
|
||||
|
||||
if ($includeAvailableProfiles) {
|
||||
// The Moiangs themselves haven't come up with anything yet with these availableProfiles
|
||||
// The Mojang themselves haven't come up with anything yet with these availableProfiles
|
||||
$availableProfiles[0] = $result['selectedProfile'];
|
||||
$result['availableProfiles'] = $availableProfiles;
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\models\authentication\LoginForm;
|
||||
@@ -7,19 +9,32 @@ use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\Module as Authserver;
|
||||
use api\modules\authserver\validators\ClientTokenValidator;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use api\rbac\Permissions as P;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
|
||||
class AuthenticationForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $username;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $clientToken;
|
||||
|
||||
public function rules() {
|
||||
public function rules(): array {
|
||||
return [
|
||||
[['username', 'password', 'clientToken'], RequiredValidator::class],
|
||||
[['clientToken'], ClientTokenValidator::class],
|
||||
@@ -28,14 +43,16 @@ class AuthenticationForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @return AuthenticateData
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
*/
|
||||
public function authenticate() {
|
||||
public function authenticate(): AuthenticateData {
|
||||
// This validating method will throw an exception in case when validation will not pass successfully
|
||||
$this->validate();
|
||||
|
||||
Authserver::info("Trying to authenticate user by login = '{$this->username}'.");
|
||||
|
||||
$loginForm = $this->createLoginForm();
|
||||
$loginForm = new LoginForm();
|
||||
$loginForm->login = $this->username;
|
||||
$loginForm->password = $this->password;
|
||||
if (!$loginForm->validate()) {
|
||||
@@ -68,37 +85,25 @@ class AuthenticationForm extends ApiForm {
|
||||
throw new ForbiddenOperationException("Invalid credentials. Invalid {$attribute} or password.");
|
||||
}
|
||||
|
||||
/** @var Account $account */
|
||||
$account = $loginForm->getAccount();
|
||||
$accessTokenModel = $this->createMinecraftAccessToken($account);
|
||||
$dataModel = new AuthenticateData($accessTokenModel);
|
||||
$token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $this->clientToken);
|
||||
$dataModel = new AuthenticateData($account, (string)$token, $this->clientToken);
|
||||
/** @var OauthSession|null $minecraftOauthSession */
|
||||
$hasMinecraftOauthSession = $account->getOauthSessions()
|
||||
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])
|
||||
->exists();
|
||||
if ($hasMinecraftOauthSession === false) {
|
||||
$minecraftOauthSession = new OauthSession();
|
||||
$minecraftOauthSession->account_id = $account->id;
|
||||
$minecraftOauthSession->client_id = OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER;
|
||||
$minecraftOauthSession->scopes = [P::MINECRAFT_SERVER_SESSION];
|
||||
Assert::true($minecraftOauthSession->save());
|
||||
}
|
||||
|
||||
Authserver::info("User with id = {$account->id}, username = '{$account->username}' and email = '{$account->email}' successfully logged in.");
|
||||
|
||||
return $dataModel;
|
||||
}
|
||||
|
||||
protected function createMinecraftAccessToken(Account $account): MinecraftAccessKey {
|
||||
/** @var MinecraftAccessKey|null $accessTokenModel */
|
||||
$accessTokenModel = MinecraftAccessKey::findOne([
|
||||
'account_id' => $account->id,
|
||||
'client_token' => $this->clientToken,
|
||||
]);
|
||||
|
||||
if ($accessTokenModel === null) {
|
||||
$accessTokenModel = new MinecraftAccessKey();
|
||||
$accessTokenModel->client_token = $this->clientToken;
|
||||
$accessTokenModel->account_id = $account->id;
|
||||
$accessTokenModel->insert();
|
||||
} else {
|
||||
$accessTokenModel->refreshPrimaryKeyValue();
|
||||
$accessTokenModel->update();
|
||||
}
|
||||
|
||||
return $accessTokenModel;
|
||||
}
|
||||
|
||||
protected function createLoginForm(): LoginForm {
|
||||
return new LoginForm();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,17 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\models\MinecraftAccessKey;
|
||||
|
||||
class InvalidateForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $accessToken;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $clientToken;
|
||||
|
||||
public function rules() {
|
||||
public function rules(): array {
|
||||
return [
|
||||
[['accessToken', 'clientToken'], RequiredValidator::class],
|
||||
];
|
||||
@@ -19,19 +26,12 @@ class InvalidateForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function invalidateToken(): bool {
|
||||
$this->validate();
|
||||
|
||||
$token = MinecraftAccessKey::findOne([
|
||||
'access_token' => $this->accessToken,
|
||||
'client_token' => $this->clientToken,
|
||||
]);
|
||||
|
||||
if ($token !== null) {
|
||||
$token->delete();
|
||||
}
|
||||
// We're can't invalidate access token because it's not stored in our database
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -1,48 +1,90 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\components\Tokens\TokenReader;
|
||||
use api\models\base\ApiForm;
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\validators\AccessTokenValidator;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
|
||||
class RefreshTokenForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $accessToken;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $clientToken;
|
||||
|
||||
public function rules() {
|
||||
public function rules(): array {
|
||||
return [
|
||||
[['accessToken', 'clientToken'], RequiredValidator::class],
|
||||
[['accessToken'], AccessTokenValidator::class, 'verifyExpiration' => false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthenticateData
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
*/
|
||||
public function refresh() {
|
||||
public function refresh(): AuthenticateData {
|
||||
$this->validate();
|
||||
$account = null;
|
||||
if (mb_strlen($this->accessToken) === 36) {
|
||||
/** @var MinecraftAccessKey $token */
|
||||
$token = MinecraftAccessKey::findOne([
|
||||
'access_token' => $this->accessToken,
|
||||
'client_token' => $this->clientToken,
|
||||
]);
|
||||
if ($token !== null) {
|
||||
$account = $token->account;
|
||||
}
|
||||
} else {
|
||||
$token = Yii::$app->tokens->parse($this->accessToken);
|
||||
$tokenReader = new TokenReader($token);
|
||||
if ($tokenReader->getMinecraftClientToken() !== $this->clientToken) {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
}
|
||||
|
||||
/** @var MinecraftAccessKey|null $accessToken */
|
||||
$accessToken = MinecraftAccessKey::findOne([
|
||||
'access_token' => $this->accessToken,
|
||||
'client_token' => $this->clientToken,
|
||||
]);
|
||||
if ($accessToken === null) {
|
||||
$account = Account::findOne(['id' => $tokenReader->getAccountId()]);
|
||||
}
|
||||
|
||||
if ($account === null) {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
}
|
||||
|
||||
if ($accessToken->account->status === Account::STATUS_BANNED) {
|
||||
if ($account->status === Account::STATUS_BANNED) {
|
||||
throw new ForbiddenOperationException('This account has been suspended.');
|
||||
}
|
||||
|
||||
$accessToken->refreshPrimaryKeyValue();
|
||||
$accessToken->update();
|
||||
$token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $this->clientToken);
|
||||
|
||||
return new AuthenticateData($accessToken);
|
||||
// TODO: This behavior duplicates with the AuthenticationForm. Need to find a way to avoid duplication.
|
||||
/** @var OauthSession|null $minecraftOauthSession */
|
||||
$hasMinecraftOauthSession = $account->getOauthSessions()
|
||||
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])
|
||||
->exists();
|
||||
if ($hasMinecraftOauthSession === false) {
|
||||
$minecraftOauthSession = new OauthSession();
|
||||
$minecraftOauthSession->account_id = $account->id;
|
||||
$minecraftOauthSession->client_id = OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER;
|
||||
$minecraftOauthSession->scopes = [P::MINECRAFT_SERVER_SESSION];
|
||||
Assert::true($minecraftOauthSession->save());
|
||||
}
|
||||
|
||||
return new AuthenticateData($account, (string)$token, $this->clientToken);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\models\authentication\LoginForm;
|
||||
@@ -6,21 +8,30 @@ use api\models\base\ApiForm;
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\MinecraftAccessKey;
|
||||
use Yii;
|
||||
|
||||
class SignoutForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $username;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $password;
|
||||
|
||||
public function rules() {
|
||||
public function rules(): array {
|
||||
return [
|
||||
[['username', 'password'], RequiredValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws ForbiddenOperationException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function signout(): bool {
|
||||
$this->validate();
|
||||
|
||||
@@ -44,16 +55,7 @@ class SignoutForm extends ApiForm {
|
||||
throw new ForbiddenOperationException("Invalid credentials. Invalid {$attribute} or password.");
|
||||
}
|
||||
|
||||
$account = $loginForm->getAccount();
|
||||
|
||||
/** @noinspection SqlResolve */
|
||||
Yii::$app->db->createCommand('
|
||||
DELETE
|
||||
FROM ' . MinecraftAccessKey::tableName() . '
|
||||
WHERE account_id = :userId
|
||||
', [
|
||||
'userId' => $account->id,
|
||||
])->execute();
|
||||
// We're unable to invalidate access tokens because they aren't stored in our database
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -1,35 +1,33 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\validators\AccessTokenValidator;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\models\MinecraftAccessKey;
|
||||
|
||||
class ValidateForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $accessToken;
|
||||
|
||||
public function rules() {
|
||||
public function rules(): array {
|
||||
return [
|
||||
[['accessToken'], RequiredValidator::class],
|
||||
[['accessToken'], AccessTokenValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function validateToken(): bool {
|
||||
$this->validate();
|
||||
|
||||
/** @var MinecraftAccessKey|null $result */
|
||||
$result = MinecraftAccessKey::findOne($this->accessToken);
|
||||
if ($result === null) {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
}
|
||||
|
||||
if ($result->isExpired()) {
|
||||
throw new ForbiddenOperationException('Token expired.');
|
||||
}
|
||||
|
||||
return true;
|
||||
return $this->validate();
|
||||
}
|
||||
|
||||
}
|
||||
|
69
api/modules/authserver/validators/AccessTokenValidator.php
Normal file
69
api/modules/authserver/validators/AccessTokenValidator.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\validators;
|
||||
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use Carbon\Carbon;
|
||||
use common\models\MinecraftAccessKey;
|
||||
use Exception;
|
||||
use Lcobucci\JWT\ValidationData;
|
||||
use Yii;
|
||||
use yii\validators\Validator;
|
||||
|
||||
class AccessTokenValidator extends Validator {
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $verifyExpiration = true;
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return array|null
|
||||
* @throws ForbiddenOperationException
|
||||
*/
|
||||
protected function validateValue($value): ?array {
|
||||
if (mb_strlen($value) === 36) {
|
||||
return $this->validateLegacyToken($value);
|
||||
}
|
||||
|
||||
try {
|
||||
$token = Yii::$app->tokens->parse($value);
|
||||
} catch (Exception $e) {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
}
|
||||
|
||||
if (!Yii::$app->tokens->verify($token)) {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
}
|
||||
|
||||
if ($this->verifyExpiration && !$token->validate(new ValidationData(Carbon::now()->getTimestamp()))) {
|
||||
throw new ForbiddenOperationException('Token expired.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return array|null
|
||||
* @throws ForbiddenOperationException
|
||||
*/
|
||||
private function validateLegacyToken(string $value): ?array {
|
||||
/** @var MinecraftAccessKey|null $result */
|
||||
$result = MinecraftAccessKey::findOne(['access_token' => $value]);
|
||||
if ($result === null) {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
}
|
||||
|
||||
if ($this->verifyExpiration && $result->isExpired()) {
|
||||
throw new ForbiddenOperationException('Token expired.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@@ -1,20 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\validators;
|
||||
|
||||
use api\modules\authserver\exceptions\IllegalArgumentException;
|
||||
use yii\validators\Validator;
|
||||
|
||||
/**
|
||||
* The maximum length of clientToken for our database is 255.
|
||||
* If the token is longer, we do not accept the passed token at all.
|
||||
*/
|
||||
class ClientTokenValidator extends \yii\validators\RequiredValidator {
|
||||
class ClientTokenValidator extends Validator {
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return null
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
*/
|
||||
protected function validateValue($value) {
|
||||
protected function validateValue($value): ?array {
|
||||
if (mb_strlen($value) > 255) {
|
||||
throw new IllegalArgumentException('clientToken is too long.');
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\authserver\validators;
|
||||
|
||||
use api\modules\authserver\exceptions\IllegalArgumentException;
|
||||
@@ -14,7 +16,7 @@ class RequiredValidator extends \yii\validators\RequiredValidator {
|
||||
* @return null
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
*/
|
||||
protected function validateValue($value) {
|
||||
protected function validateValue($value): ?array {
|
||||
if (parent::validateValue($value) !== null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
@@ -1,9 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\oauth\controllers;
|
||||
|
||||
use api\controllers\Controller;
|
||||
use api\modules\oauth\models\OauthProcess;
|
||||
use api\rbac\Permissions as P;
|
||||
use GuzzleHttp\Psr7\ServerRequest;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Yii;
|
||||
use yii\filters\AccessControl;
|
||||
use yii\helpers\ArrayHelper;
|
||||
@@ -43,22 +47,23 @@ class AuthorizationController extends Controller {
|
||||
}
|
||||
|
||||
public function actionValidate(): array {
|
||||
return $this->createOauthProcess()->validate();
|
||||
return $this->createOauthProcess()->validate($this->getServerRequest());
|
||||
}
|
||||
|
||||
public function actionComplete(): array {
|
||||
return $this->createOauthProcess()->complete();
|
||||
return $this->createOauthProcess()->complete($this->getServerRequest());
|
||||
}
|
||||
|
||||
public function actionToken(): array {
|
||||
return $this->createOauthProcess()->getToken();
|
||||
return $this->createOauthProcess()->getToken($this->getServerRequest());
|
||||
}
|
||||
|
||||
private function createOauthProcess(): OauthProcess {
|
||||
$server = Yii::$app->oauth->authServer;
|
||||
$server->setRequest(null); // Enforce request recreation (test environment bug)
|
||||
return new OauthProcess(Yii::$app->oauth->getAuthServer());
|
||||
}
|
||||
|
||||
return new OauthProcess($server);
|
||||
private function getServerRequest(): ServerRequestInterface {
|
||||
return ServerRequest::fromGlobals();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,19 +1,22 @@
|
||||
<?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\components\OAuth2\Entities\UserEntity;
|
||||
use api\components\OAuth2\Events\RequestedRefreshToken;
|
||||
use api\rbac\Permissions as P;
|
||||
use common\models\Account;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
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\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Exception\OAuthServerException;
|
||||
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class OauthProcess {
|
||||
|
||||
@@ -46,21 +49,18 @@ class OauthProcess {
|
||||
*
|
||||
* In addition, you can pass the description value to override the application's description.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
public function validate(): array {
|
||||
public function validate(ServerRequestInterface $request): array {
|
||||
try {
|
||||
$authParams = $this->getAuthorizationCodeGrant()->checkAuthorizeParams();
|
||||
$client = $authParams->getClient();
|
||||
$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) {
|
||||
$response = $this->buildErrorResponse($e);
|
||||
$clientModel = $this->findClient($client->getIdentifier());
|
||||
$response = $this->buildSuccessResponse($request, $clientModel, $authRequest->getScopes());
|
||||
} catch (OAuthServerException $e) {
|
||||
$response = $this->buildCompleteErrorResponse($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
@@ -83,45 +83,53 @@ class OauthProcess {
|
||||
* If the field is present, it will be interpreted as any value resulting in false positives.
|
||||
* Otherwise, the value will be interpreted as "true".
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
public function complete(): array {
|
||||
public function complete(ServerRequestInterface $request): array {
|
||||
try {
|
||||
Yii::$app->statsd->inc('oauth.complete.attempt');
|
||||
$grant = $this->getAuthorizationCodeGrant();
|
||||
$authParams = $grant->checkAuthorizeParams();
|
||||
|
||||
$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 $client */
|
||||
$client = $this->findClient($authRequest->getClient()->getIdentifier());
|
||||
|
||||
if (!$this->canAutoApprove($account, $clientModel, $authParams)) {
|
||||
$approved = $this->canAutoApprove($account, $client, $authRequest);
|
||||
if (!$approved) {
|
||||
Yii::$app->statsd->inc('oauth.complete.approve_required');
|
||||
$isAccept = Yii::$app->request->post('accept');
|
||||
if ($isAccept === null) {
|
||||
throw new AcceptRequiredException();
|
||||
|
||||
$acceptParam = ((array)$request->getParsedBody())['accept'] ?? null;
|
||||
if ($acceptParam === null) {
|
||||
throw $this->createAcceptRequiredException();
|
||||
}
|
||||
|
||||
if (!$isAccept) {
|
||||
throw new AccessDeniedException($authParams->getRedirectUri());
|
||||
$approved = in_array($acceptParam, [1, '1', true, 'true'], true);
|
||||
if ($approved) {
|
||||
$this->storeOauthSession($account, $client, $authRequest);
|
||||
}
|
||||
}
|
||||
|
||||
$redirectUri = $grant->newAuthorizeRequest('user', $account->id, $authParams);
|
||||
$response = [
|
||||
$authRequest->setUser(new UserEntity($account->id));
|
||||
$authRequest->setAuthorizationApproved($approved);
|
||||
$response = $this->server->completeAuthorizationRequest($authRequest, new Response(200));
|
||||
|
||||
$result = [
|
||||
'success' => true,
|
||||
'redirectUri' => $redirectUri,
|
||||
'redirectUri' => $response->getHeaderLine('Location'),
|
||||
];
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
$response = $this->buildErrorResponse($e);
|
||||
$result = $this->buildCompleteErrorResponse($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,30 +151,44 @@ class OauthProcess {
|
||||
* grant_type,
|
||||
* ]
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
public function getToken(): array {
|
||||
$grantType = Yii::$app->request->post('grant_type', 'null');
|
||||
public function getToken(ServerRequestInterface $request): array {
|
||||
$params = (array)$request->getParsedBody();
|
||||
$clientId = $params['client_id'] ?? '';
|
||||
$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');
|
||||
|
||||
$shouldIssueRefreshToken = false;
|
||||
$this->server->getEmitter()->addOneTimeListener(RequestedRefreshToken::class, function() use (&$shouldIssueRefreshToken) {
|
||||
$shouldIssueRefreshToken = true;
|
||||
});
|
||||
|
||||
$response = $this->server->respondToAccessTokenRequest($request, new Response(200));
|
||||
/** @noinspection JsonEncodingApiUsageInspection at this point json error is not possible */
|
||||
$result = json_decode((string)$response->getBody(), true);
|
||||
if ($shouldIssueRefreshToken) {
|
||||
// Set the refresh_token field to keep compatibility with the old clients,
|
||||
// which will be broken in case when refresh_token field will be missing
|
||||
$result['refresh_token'] = $result['access_token'];
|
||||
}
|
||||
|
||||
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;
|
||||
$response = [
|
||||
'error' => $e->errorType,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
Yii::$app->response->statusCode = $e->getHttpStatusCode();
|
||||
|
||||
$result = $this->buildIssueErrorResponse($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function findClient(string $clientId): ?OauthClient {
|
||||
return OauthClient::findOne($clientId);
|
||||
return OauthClient::findOne(['id' => $clientId]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,39 +197,48 @@ 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;
|
||||
}
|
||||
|
||||
/** @var \common\models\OauthSession|null $session */
|
||||
$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))) {
|
||||
return true;
|
||||
}
|
||||
$session = $this->findOauthSession($account, $client);
|
||||
if ($session === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return empty(array_diff($this->getScopesList($request), $session->getScopes()));
|
||||
}
|
||||
|
||||
private function storeOauthSession(Account $account, OauthClient $client, AuthorizationRequest $request): void {
|
||||
$session = $this->findOauthSession($account, $client);
|
||||
if ($session === null) {
|
||||
$session = new OauthSession();
|
||||
$session->account_id = $account->id;
|
||||
$session->client_id = $client->id;
|
||||
}
|
||||
|
||||
$session->scopes = array_unique(array_merge($session->getScopes(), $this->getScopesList($request)));
|
||||
|
||||
Assert::true($session->save());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $queryParams
|
||||
* @param ServerRequestInterface $request
|
||||
* @param OauthClient $client
|
||||
* @param \api\components\OAuth2\Entities\ScopeEntity[] $scopes
|
||||
* @param 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 +248,94 @@ 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 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 buildCompleteErrorResponse(OAuthServerException $e): array {
|
||||
$hint = $e->getPayload()['hint'] ?? '';
|
||||
if (preg_match('/the `(\w+)` scope/', $hint, $matches)) {
|
||||
$parameter = $matches[1];
|
||||
}
|
||||
|
||||
$response = [
|
||||
'success' => false,
|
||||
'error' => $e->errorType,
|
||||
'parameter' => $e->parameter,
|
||||
'statusCode' => $e->httpStatusCode,
|
||||
'error' => $e->getErrorType(),
|
||||
'parameter' => $parameter ?? null,
|
||||
'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 getAuthorizationCodeGrant(): AuthCodeGrant {
|
||||
/** @var GrantTypeInterface $grantType */
|
||||
$grantType = $this->getGrant('authorization_code');
|
||||
if (!$grantType instanceof AuthCodeGrant) {
|
||||
throw new InvalidGrantException('authorization_code grant have invalid realisation');
|
||||
/**
|
||||
* Raw error messages aren't very informative for the end user, as they don't contain
|
||||
* information about the parameter that caused the error.
|
||||
* This method is intended to build a more understandable description.
|
||||
*
|
||||
* Part of the existing texts are the legacy from the previous implementation.
|
||||
*
|
||||
* @param OAuthServerException $e
|
||||
* @return array
|
||||
*/
|
||||
private function buildIssueErrorResponse(OAuthServerException $e): array {
|
||||
$errorType = $e->getErrorType();
|
||||
$message = $e->getMessage();
|
||||
$hint = $e->getHint();
|
||||
switch ($hint) {
|
||||
case 'Invalid redirect URI':
|
||||
$errorType = 'invalid_client';
|
||||
$message = 'Client authentication failed.';
|
||||
break;
|
||||
case 'Cannot decrypt the authorization code':
|
||||
$message .= ' Check the "code" parameter.';
|
||||
break;
|
||||
}
|
||||
|
||||
return $grantType;
|
||||
return [
|
||||
'error' => $errorType,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
private function createAcceptRequiredException(): OAuthServerException {
|
||||
return new OAuthServerException('Client must accept authentication request.', 0, 'accept_required', 401);
|
||||
}
|
||||
|
||||
private function getScopesList(AuthorizationRequest $request): array {
|
||||
return array_map(function(ScopeEntityInterface $scope): string {
|
||||
return $scope->getIdentifier();
|
||||
}, $request->getScopes());
|
||||
}
|
||||
|
||||
private function findOauthSession(Account $account, OauthClient $client): ?OauthSession {
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||
return $account->getOauthSessions()->andWhere(['client_id' => $client->id])->one();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\session\controllers;
|
||||
|
||||
use api\controllers\Controller;
|
||||
@@ -31,18 +33,23 @@ class SessionController extends Controller {
|
||||
return $behaviors;
|
||||
}
|
||||
|
||||
public function actionJoin() {
|
||||
/**
|
||||
* @return array
|
||||
* @throws ForbiddenOperationException
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public function actionJoin(): array {
|
||||
Yii::$app->response->format = Response::FORMAT_JSON;
|
||||
|
||||
$data = Yii::$app->request->post();
|
||||
$protocol = new ModernJoin($data['accessToken'] ?? '', $data['selectedProfile'] ?? '', $data['serverId'] ?? '');
|
||||
$joinForm = new JoinForm($protocol);
|
||||
$joinForm->join();
|
||||
$joinForm->join(); // will throw an exception in case of any error
|
||||
|
||||
return ['id' => 'OK'];
|
||||
}
|
||||
|
||||
public function actionJoinLegacy() {
|
||||
public function actionJoinLegacy(): string {
|
||||
Yii::$app->response->format = Response::FORMAT_RAW;
|
||||
|
||||
$data = Yii::$app->request->get();
|
||||
@@ -64,7 +71,12 @@ class SessionController extends Controller {
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
public function actionHasJoined() {
|
||||
/**
|
||||
* @return array
|
||||
* @throws ForbiddenOperationException
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public function actionHasJoined(): array {
|
||||
Yii::$app->response->format = Response::FORMAT_JSON;
|
||||
|
||||
$data = Yii::$app->request->get();
|
||||
@@ -76,7 +88,7 @@ class SessionController extends Controller {
|
||||
return $textures->getMinecraftResponse();
|
||||
}
|
||||
|
||||
public function actionHasJoinedLegacy() {
|
||||
public function actionHasJoinedLegacy(): string {
|
||||
Yii::$app->response->format = Response::FORMAT_RAW;
|
||||
|
||||
$data = Yii::$app->request->get();
|
||||
@@ -95,7 +107,14 @@ class SessionController extends Controller {
|
||||
return 'YES';
|
||||
}
|
||||
|
||||
public function actionProfile($uuid) {
|
||||
/**
|
||||
* @param string $uuid
|
||||
*
|
||||
* @return array
|
||||
* @throws ForbiddenOperationException
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public function actionProfile(string $uuid): array {
|
||||
try {
|
||||
$uuid = Uuid::fromString($uuid)->toString();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\session\models;
|
||||
|
||||
use api\modules\session\exceptions\ForbiddenOperationException;
|
||||
@@ -6,19 +8,27 @@ use api\modules\session\exceptions\IllegalArgumentException;
|
||||
use api\modules\session\models\protocols\HasJoinedInterface;
|
||||
use api\modules\session\Module as Session;
|
||||
use common\models\Account;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
use yii\base\Model;
|
||||
|
||||
class HasJoinedForm extends Model {
|
||||
|
||||
/**
|
||||
* @var HasJoinedInterface
|
||||
*/
|
||||
private $protocol;
|
||||
|
||||
public function __construct(HasJoinedInterface $protocol, array $config = []) {
|
||||
$this->protocol = $protocol;
|
||||
parent::__construct($config);
|
||||
$this->protocol = $protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account
|
||||
* @throws ForbiddenOperationException
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public function hasJoined(): Account {
|
||||
Yii::$app->statsd->inc('sessionserver.hasJoined.attempt');
|
||||
if (!$this->protocol->validate()) {
|
||||
@@ -38,10 +48,9 @@ class HasJoinedForm extends Model {
|
||||
}
|
||||
|
||||
$joinModel->delete();
|
||||
/** @var Account $account */
|
||||
$account = $joinModel->getAccount();
|
||||
if ($account === null) {
|
||||
throw new ErrorException('Account must exists');
|
||||
}
|
||||
Assert::notNull($account);
|
||||
|
||||
Session::info("User with username = '{$username}' successfully verified by server with server_id = '{$serverId}'.");
|
||||
Yii::$app->statsd->inc('sessionserver.hasJoined.success');
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\modules\session\models;
|
||||
|
||||
use api\modules\session\exceptions\ForbiddenOperationException;
|
||||
@@ -11,8 +13,8 @@ use common\helpers\StringHelper;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
use yii\base\Model;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
@@ -35,15 +37,14 @@ class JoinForm extends Model {
|
||||
private $protocol;
|
||||
|
||||
public function __construct(JoinInterface $protocol, array $config = []) {
|
||||
parent::__construct($config);
|
||||
$this->protocol = $protocol;
|
||||
$this->accessToken = $protocol->getAccessToken();
|
||||
$this->selectedProfile = $protocol->getSelectedProfile();
|
||||
$this->serverId = $protocol->getServerId();
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function rules() {
|
||||
public function rules(): array {
|
||||
return [
|
||||
[['accessToken', 'serverId'], RequiredValidator::class],
|
||||
[['accessToken', 'selectedProfile'], 'validateUuid'],
|
||||
@@ -51,7 +52,12 @@ class JoinForm extends Model {
|
||||
];
|
||||
}
|
||||
|
||||
public function join() {
|
||||
/**
|
||||
* @return bool
|
||||
* @throws IllegalArgumentException
|
||||
* @throws ForbiddenOperationException
|
||||
*/
|
||||
public function join(): bool {
|
||||
$serverId = $this->serverId;
|
||||
$accessToken = $this->accessToken;
|
||||
Session::info("User with access_token = '{$accessToken}' trying join to server with server_id = '{$serverId}'.");
|
||||
@@ -62,9 +68,7 @@ class JoinForm extends Model {
|
||||
|
||||
$account = $this->getAccount();
|
||||
$sessionModel = new SessionModel($account->username, $serverId);
|
||||
if (!$sessionModel->save()) {
|
||||
throw new ErrorException('Cannot save join session model');
|
||||
}
|
||||
Assert::true($sessionModel->save());
|
||||
|
||||
Session::info("User with access_token = '{$accessToken}' and nickname = '{$account->username}' successfully joined to server_id = '{$serverId}'.");
|
||||
Yii::$app->statsd->inc('sessionserver.join.success');
|
||||
@@ -72,7 +76,14 @@ class JoinForm extends Model {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validate($attributeNames = null, $clearErrors = true) {
|
||||
/**
|
||||
* @param string $attributeNames
|
||||
* @param bool $clearErrors
|
||||
*
|
||||
* @return bool
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public function validate($attributeNames = null, $clearErrors = true): bool {
|
||||
if (!$this->protocol->validate()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
@@ -80,7 +91,12 @@ class JoinForm extends Model {
|
||||
return parent::validate($attributeNames, $clearErrors);
|
||||
}
|
||||
|
||||
public function validateUuid($attribute) {
|
||||
/**
|
||||
* @param string $attribute
|
||||
*
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public function validateUuid(string $attribute): void {
|
||||
if ($this->hasErrors($attribute)) {
|
||||
return;
|
||||
}
|
||||
@@ -91,18 +107,19 @@ class JoinForm extends Model {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \api\modules\session\exceptions\SessionServerException
|
||||
* @throws \api\modules\session\exceptions\ForbiddenOperationException
|
||||
*/
|
||||
public function validateAccessToken() {
|
||||
public function validateAccessToken(): void {
|
||||
$accessToken = $this->accessToken;
|
||||
/** @var MinecraftAccessKey|null $accessModel */
|
||||
$accessModel = MinecraftAccessKey::findOne($accessToken);
|
||||
$accessModel = MinecraftAccessKey::findOne(['access_token' => $accessToken]);
|
||||
if ($accessModel !== null) {
|
||||
Yii::$app->statsd->inc('sessionserver.authentication.legacy_minecraft_protocol');
|
||||
/** @var MinecraftAccessKey|\api\components\OAuth2\Entities\AccessTokenEntity $accessModel */
|
||||
if ($accessModel->isExpired()) {
|
||||
Session::error("User with access_token = '{$accessToken}' failed join by expired access_token.");
|
||||
Yii::$app->statsd->inc('sessionserver.authentication.legacy_minecraft_protocol_token_expired');
|
||||
|
||||
throw new ForbiddenOperationException('Expired access_token.');
|
||||
}
|
||||
|
||||
@@ -117,6 +134,7 @@ class JoinForm extends Model {
|
||||
if ($identity === null) {
|
||||
Session::error("User with access_token = '{$accessToken}' failed join by wrong access_token.");
|
||||
Yii::$app->statsd->inc('sessionserver.join.fail_wrong_token');
|
||||
|
||||
throw new ForbiddenOperationException('Invalid access_token.');
|
||||
}
|
||||
|
||||
@@ -124,6 +142,7 @@ class JoinForm extends Model {
|
||||
if (!Yii::$app->user->can(P::MINECRAFT_SERVER_SESSION)) {
|
||||
Session::error("User with access_token = '{$accessToken}' doesn't have enough scopes to make join.");
|
||||
Yii::$app->statsd->inc('sessionserver.authentication.oauth2_not_enough_scopes');
|
||||
|
||||
throw new ForbiddenOperationException('The token does not have required scope.');
|
||||
}
|
||||
|
||||
@@ -135,12 +154,14 @@ class JoinForm extends Model {
|
||||
if ($isUuid && $account->uuid !== $this->normalizeUUID($selectedProfile)) {
|
||||
Session::error("User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}', but access_token issued to account with id = '{$account->uuid}'.");
|
||||
Yii::$app->statsd->inc('sessionserver.join.fail_uuid_mismatch');
|
||||
|
||||
throw new ForbiddenOperationException('Wrong selected_profile.');
|
||||
}
|
||||
|
||||
if (!$isUuid && mb_strtolower($account->username) !== mb_strtolower($selectedProfile)) {
|
||||
Session::error("User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}', but access_token issued to account with username = '{$account->username}'.");
|
||||
Yii::$app->statsd->inc('sessionserver.join.fail_username_mismatch');
|
||||
|
||||
throw new ForbiddenOperationException('Invalid credentials');
|
||||
}
|
||||
|
||||
|
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
namespace api\tests\_pages;
|
||||
|
||||
class AuthserverRoute extends BasePage {
|
||||
|
||||
public function authenticate($params) {
|
||||
$this->getActor()->sendPOST('/api/authserver/authentication/authenticate', $params);
|
||||
}
|
||||
|
||||
public function refresh($params) {
|
||||
$this->getActor()->sendPOST('/api/authserver/authentication/refresh', $params);
|
||||
}
|
||||
|
||||
public function validate($params) {
|
||||
$this->getActor()->sendPOST('/api/authserver/authentication/validate', $params);
|
||||
}
|
||||
|
||||
public function invalidate($params) {
|
||||
$this->getActor()->sendPOST('/api/authserver/authentication/invalidate', $params);
|
||||
}
|
||||
|
||||
public function signout($params) {
|
||||
$this->getActor()->sendPOST('/api/authserver/authentication/signout', $params);
|
||||
}
|
||||
|
||||
}
|
@@ -1,40 +1,52 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\_pages;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* TODO: remove
|
||||
*/
|
||||
class OauthRoute extends BasePage {
|
||||
|
||||
public function validate(array $queryParams): void {
|
||||
$this->getActor()->sendGET('/api/oauth2/v1/validate', $queryParams);
|
||||
}
|
||||
|
||||
public function complete(array $queryParams = [], array $postParams = []): void {
|
||||
$this->getActor()->sendPOST('/api/oauth2/v1/complete?' . http_build_query($queryParams), $postParams);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests;
|
||||
|
||||
use api\components\Tokens\TokensFactory;
|
||||
use api\tests\_generated\FunctionalTesterActions;
|
||||
use Codeception\Actor;
|
||||
use common\models\Account;
|
||||
@@ -20,7 +19,7 @@ class FunctionalTester extends Actor {
|
||||
throw new InvalidArgumentException("Cannot find account with username \"{$asUsername}\"");
|
||||
}
|
||||
|
||||
$token = TokensFactory::createForAccount($account);
|
||||
$token = Yii::$app->tokensFactory->createForWebAccount($account);
|
||||
$this->amBearerAuthenticated((string)$token);
|
||||
|
||||
return $account->id;
|
||||
|
@@ -17,4 +17,4 @@ modules:
|
||||
host: redis
|
||||
port: 6379
|
||||
database: 0
|
||||
cleanupBefore: 'test'
|
||||
cleanupBefore: 'suite'
|
||||
|
@@ -3,16 +3,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\_steps;
|
||||
|
||||
use api\tests\_pages\AuthserverRoute;
|
||||
use api\tests\FunctionalTester;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class AuthserverSteps extends FunctionalTester {
|
||||
|
||||
public function amAuthenticated(string $asUsername = 'admin', string $password = 'password_0') {
|
||||
$route = new AuthserverRoute($this);
|
||||
public function amAuthenticated(string $asUsername = 'admin', string $password = 'password_0'): array {
|
||||
$clientToken = Uuid::uuid4()->toString();
|
||||
$route->authenticate([
|
||||
$this->sendPOST('/api/authserver/authentication/authenticate', [
|
||||
'username' => $asUsername,
|
||||
'password' => $password,
|
||||
'clientToken' => $clientToken,
|
||||
|
@@ -1,62 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\_steps;
|
||||
|
||||
use api\components\OAuth2\Storage\ScopeStorage as S;
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\components\OAuth2\Repositories\PublicScopeRepository;
|
||||
use api\tests\FunctionalTester;
|
||||
|
||||
class OauthSteps extends FunctionalTester {
|
||||
|
||||
public function getAuthCode(array $permissions = []) {
|
||||
public function obtainAuthCode(array $permissions = []): string {
|
||||
$this->amAuthenticated();
|
||||
$route = new OauthRoute($this);
|
||||
$route->complete([
|
||||
$this->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => implode(',', $permissions),
|
||||
], ['accept' => true]);
|
||||
'scope' => implode(' ', $permissions),
|
||||
]), ['accept' => true]);
|
||||
$this->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
$response = json_decode($this->grabResponse(), true);
|
||||
preg_match('/code=([\w-]+)/', $response['redirectUri'], $matches);
|
||||
[$redirectUri] = $this->grabDataFromResponseByJsonPath('$.redirectUri');
|
||||
preg_match('/code=([^&$]+)/', $redirectUri, $matches);
|
||||
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
public function getAccessToken(array $permissions = []) {
|
||||
$authCode = $this->getAuthCode($permissions);
|
||||
public function getAccessToken(array $permissions = []): string {
|
||||
$authCode = $this->obtainAuthCode($permissions);
|
||||
$response = $this->issueToken($authCode);
|
||||
|
||||
return $response['access_token'];
|
||||
}
|
||||
|
||||
public function getRefreshToken(array $permissions = []) {
|
||||
$authCode = $this->getAuthCode(array_merge([S::OFFLINE_ACCESS], $permissions));
|
||||
public function getRefreshToken(array $permissions = []): string {
|
||||
$authCode = $this->obtainAuthCode(array_merge([PublicScopeRepository::OFFLINE_ACCESS], $permissions));
|
||||
$response = $this->issueToken($authCode);
|
||||
|
||||
return $response['refresh_token'];
|
||||
}
|
||||
|
||||
public function issueToken($authCode) {
|
||||
$route = new OauthRoute($this);
|
||||
$route->issueToken([
|
||||
public function issueToken(string $authCode): array {
|
||||
$this->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $authCode,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'grant_type' => 'authorization_code',
|
||||
]);
|
||||
|
||||
return json_decode($this->grabResponse(), true);
|
||||
}
|
||||
|
||||
public function getAccessTokenByClientCredentialsGrant(array $permissions = [], $useTrusted = true) {
|
||||
$route = new OauthRoute($this);
|
||||
$route->issueToken([
|
||||
public function getAccessTokenByClientCredentialsGrant(array $permissions = [], bool $useTrusted = true): string {
|
||||
$this->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => $useTrusted ? 'trusted-client' : 'default-client',
|
||||
'client_secret' => $useTrusted ? 'tXBbyvMcyaOgHMOAXBpN2EC7uFoJAaL9' : 'AzWRy7ZjS1yRQUk2vRBDic8fprOKDB1W',
|
||||
'grant_type' => 'client_credentials',
|
||||
'scope' => implode(',', $permissions),
|
||||
'scope' => implode(' ', $permissions),
|
||||
]);
|
||||
|
||||
$response = json_decode($this->grabResponse(), true);
|
||||
|
@@ -1,48 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\authserver;
|
||||
|
||||
use api\tests\_pages\AuthserverRoute;
|
||||
use api\tests\FunctionalTester;
|
||||
use Codeception\Example;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class AuthorizationCest {
|
||||
|
||||
/**
|
||||
* @var AuthserverRoute
|
||||
* @example {"login": "admin", "password": "password_0"}
|
||||
* @example {"login": "admin@ely.by", "password": "password_0"}
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new AuthserverRoute($I);
|
||||
}
|
||||
|
||||
public function byName(FunctionalTester $I) {
|
||||
public function byFormParamsPostRequest(FunctionalTester $I, Example $example) {
|
||||
$I->wantTo('authenticate by username and password');
|
||||
$this->route->authenticate([
|
||||
'username' => 'admin',
|
||||
'password' => 'password_0',
|
||||
$I->sendPOST('/api/authserver/authentication/authenticate', [
|
||||
'username' => $example['login'],
|
||||
'password' => $example['password'],
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
|
||||
$this->testSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function byEmail(FunctionalTester $I) {
|
||||
$I->wantTo('authenticate by email and password');
|
||||
$this->route->authenticate([
|
||||
'username' => 'admin@ely.by',
|
||||
'password' => 'password_0',
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
|
||||
$this->testSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function byNamePassedViaPOSTBody(FunctionalTester $I) {
|
||||
/**
|
||||
* @example {"login": "admin", "password": "password_0"}
|
||||
* @example {"login": "admin@ely.by", "password": "password_0"}
|
||||
*/
|
||||
public function byJsonPostRequest(FunctionalTester $I, Example $example) {
|
||||
$I->wantTo('authenticate by username and password sent via post body');
|
||||
$this->route->authenticate(json_encode([
|
||||
'username' => 'admin',
|
||||
'password' => 'password_0',
|
||||
$I->sendPOST('/api/authserver/authentication/authenticate', json_encode([
|
||||
'username' => $example['login'],
|
||||
'password' => $example['password'],
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]));
|
||||
|
||||
@@ -51,7 +41,7 @@ class AuthorizationCest {
|
||||
|
||||
public function byEmailWithEnabledTwoFactorAuth(FunctionalTester $I) {
|
||||
$I->wantTo('get valid error by authenticate account with enabled two factor auth');
|
||||
$this->route->authenticate([
|
||||
$I->sendPOST('/api/authserver/authentication/authenticate', [
|
||||
'username' => 'otp@gmail.com',
|
||||
'password' => 'password_0',
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
@@ -64,30 +54,9 @@ class AuthorizationCest {
|
||||
]);
|
||||
}
|
||||
|
||||
public function byEmailWithParamsAsJsonInPostBody(FunctionalTester $I) {
|
||||
$I->wantTo('authenticate by email and password, passing values as serialized string in post body');
|
||||
$this->route->authenticate(json_encode([
|
||||
'username' => 'admin@ely.by',
|
||||
'password' => 'password_0',
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]));
|
||||
|
||||
$this->testSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function longClientToken(FunctionalTester $I) {
|
||||
$I->wantTo('send non uuid clientToken, but less then 255 characters');
|
||||
$this->route->authenticate([
|
||||
'username' => 'admin@ely.by',
|
||||
'password' => 'password_0',
|
||||
'clientToken' => str_pad('', 255, 'x'),
|
||||
]);
|
||||
$this->testSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function tooLongClientToken(FunctionalTester $I) {
|
||||
$I->wantTo('send non uuid clientToken with more then 255 characters length');
|
||||
$this->route->authenticate([
|
||||
$I->sendPOST('/api/authserver/authentication/authenticate', [
|
||||
'username' => 'admin@ely.by',
|
||||
'password' => 'password_0',
|
||||
'clientToken' => str_pad('', 256, 'x'),
|
||||
@@ -102,7 +71,7 @@ class AuthorizationCest {
|
||||
|
||||
public function wrongArguments(FunctionalTester $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->authenticate([
|
||||
$I->sendPOST('/api/authserver/authentication/authenticate', [
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
@@ -115,7 +84,7 @@ class AuthorizationCest {
|
||||
|
||||
public function wrongNicknameAndPassword(FunctionalTester $I) {
|
||||
$I->wantTo('authenticate by username and password with wrong data');
|
||||
$this->route->authenticate([
|
||||
$I->sendPOST('/api/authserver/authentication/authenticate', [
|
||||
'username' => 'nonexistent_user',
|
||||
'password' => 'nonexistent_password',
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
@@ -130,7 +99,7 @@ class AuthorizationCest {
|
||||
|
||||
public function bannedAccount(FunctionalTester $I) {
|
||||
$I->wantTo('authenticate in suspended account');
|
||||
$this->route->authenticate([
|
||||
$I->sendPOST('/api/authserver/authentication/authenticate', [
|
||||
'username' => 'Banned',
|
||||
'password' => 'password_0',
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
|
@@ -3,25 +3,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\authserver;
|
||||
|
||||
use api\tests\_pages\AuthserverRoute;
|
||||
use api\tests\functional\_steps\AuthserverSteps;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class InvalidateCest {
|
||||
|
||||
/**
|
||||
* @var AuthserverRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(AuthserverSteps $I) {
|
||||
$this->route = new AuthserverRoute($I);
|
||||
}
|
||||
|
||||
public function invalidate(AuthserverSteps $I) {
|
||||
$I->wantTo('invalidate my token');
|
||||
[$accessToken, $clientToken] = $I->amAuthenticated();
|
||||
$this->route->invalidate([
|
||||
$I->sendPOST('/api/authserver/authentication/invalidate', [
|
||||
'accessToken' => $accessToken,
|
||||
'clientToken' => $clientToken,
|
||||
]);
|
||||
@@ -31,7 +21,7 @@ class InvalidateCest {
|
||||
|
||||
public function wrongArguments(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->invalidate([
|
||||
$I->sendPOST('/api/authserver/authentication/invalidate', [
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
@@ -44,7 +34,7 @@ class InvalidateCest {
|
||||
|
||||
public function wrongAccessTokenOrClientToken(AuthserverSteps $I) {
|
||||
$I->wantTo('invalidate by wrong client and access token');
|
||||
$this->route->invalidate([
|
||||
$I->sendPOST('/api/authserver/authentication/invalidate', [
|
||||
'accessToken' => Uuid::uuid4()->toString(),
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
|
@@ -1,42 +1,74 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\authserver;
|
||||
|
||||
use api\tests\_pages\AuthserverRoute;
|
||||
use api\tests\functional\_steps\AuthserverSteps;
|
||||
use Codeception\Example;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class RefreshCest {
|
||||
|
||||
/**
|
||||
* @var AuthserverRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(AuthserverSteps $I) {
|
||||
$this->route = new AuthserverRoute($I);
|
||||
}
|
||||
|
||||
public function refresh(AuthserverSteps $I) {
|
||||
$I->wantTo('refresh my accessToken');
|
||||
$I->wantTo('refresh accessToken');
|
||||
[$accessToken, $clientToken] = $I->amAuthenticated();
|
||||
$this->route->refresh([
|
||||
$I->sendPOST('/api/authserver/authentication/refresh', [
|
||||
'accessToken' => $accessToken,
|
||||
'clientToken' => $clientToken,
|
||||
]);
|
||||
$this->assertSuccessResponse($I);
|
||||
}
|
||||
|
||||
$I->seeResponseCodeIs(200);
|
||||
$I->seeResponseIsJson();
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.accessToken');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.clientToken');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.id');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.name');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.legacy');
|
||||
$I->cantSeeResponseJsonMatchesJsonPath('$.availableProfiles');
|
||||
public function refreshLegacyAccessToken(AuthserverSteps $I) {
|
||||
$I->wantTo('refresh legacy accessToken');
|
||||
$I->sendPOST('/api/authserver/authentication/refresh', [
|
||||
'accessToken' => 'e7bb6648-2183-4981-9b86-eba5e7f87b42',
|
||||
'clientToken' => '6f380440-0c05-47bd-b7c6-d011f1b5308f',
|
||||
]);
|
||||
$this->assertSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function refreshWithInvalidClientToken(AuthserverSteps $I) {
|
||||
$I->wantTo('refresh accessToken with not matched client token');
|
||||
[$accessToken] = $I->amAuthenticated();
|
||||
$I->sendPOST('/api/authserver/authentication/refresh', [
|
||||
'accessToken' => $accessToken,
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Invalid token.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function refreshLegacyAccessTokenWithInvalidClientToken(AuthserverSteps $I) {
|
||||
$I->wantTo('refresh legacy accessToken with not matched client token');
|
||||
$I->sendPOST('/api/authserver/authentication/refresh', [
|
||||
'accessToken' => 'e7bb6648-2183-4981-9b86-eba5e7f87b42',
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Invalid token.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example {"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE1NzU1NjE1MjgsImV4cCI6MTU3NTU2MTUyOCwiZWx5LXNjb3BlcyI6Im1pbmVjcmFmdF9zZXJ2ZXJfc2Vzc2lvbiIsImVseS1jbGllbnQtdG9rZW4iOiIydnByWnRVdk40VTVtSnZzc0ozaXNpekdVWFhQYnFsV1FsQjVVRWVfUV81bkxKYzlsbUJ3VU1hQWJ1MjBtZC1FNzNtengxNWFsZmRJSU1OMTV5YUpBalZOM29vQW9IRDctOWdOcmciLCJzdWIiOiJlbHl8MSJ9.vwjXzy0VtjJlP6B4RxqoE69yRSBsluZ29VELe4vDi8GCy487eC5cIf9hz9oxp5YcdE7uEJZeqX2yi3nk_0nCaA", "clientToken": "4f368b58-9097-4e56-80b1-f421ae4b53cf"}
|
||||
* @example {"accessToken": "6042634a-a1e2-4aed-866c-c661fe4e63e2", "clientToken": "47fb164a-2332-42c1-8bad-549e67bb210c"}
|
||||
*/
|
||||
public function refreshExpiredToken(AuthserverSteps $I, Example $example) {
|
||||
$I->wantTo('refresh legacy accessToken');
|
||||
$I->sendPOST('/api/authserver/authentication/refresh', [
|
||||
'accessToken' => $example['accessToken'],
|
||||
'clientToken' => $example['clientToken'],
|
||||
]);
|
||||
$this->assertSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function wrongArguments(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->refresh([
|
||||
$I->sendPOST('/api/authserver/authentication/refresh', [
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
@@ -49,7 +81,7 @@ class RefreshCest {
|
||||
|
||||
public function wrongAccessToken(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong access or client tokens');
|
||||
$this->route->refresh([
|
||||
$I->sendPOST('/api/authserver/authentication/refresh', [
|
||||
'accessToken' => Uuid::uuid4()->toString(),
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
@@ -63,7 +95,7 @@ class RefreshCest {
|
||||
|
||||
public function refreshTokenFromBannedUser(AuthserverSteps $I) {
|
||||
$I->wantTo('refresh token from suspended account');
|
||||
$this->route->refresh([
|
||||
$I->sendPOST('/api/authserver/authentication/refresh', [
|
||||
'accessToken' => '918ecb41-616c-40ee-a7d2-0b0ef0d0d732',
|
||||
'clientToken' => '6042634a-a1e2-4aed-866c-c661fe4e63e2',
|
||||
]);
|
||||
@@ -74,4 +106,15 @@ class RefreshCest {
|
||||
]);
|
||||
}
|
||||
|
||||
private function assertSuccessResponse(AuthserverSteps $I) {
|
||||
$I->seeResponseCodeIs(200);
|
||||
$I->seeResponseIsJson();
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.accessToken');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.clientToken');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.id');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.name');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.legacy');
|
||||
$I->cantSeeResponseJsonMatchesJsonPath('$.availableProfiles');
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,35 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\authserver;
|
||||
|
||||
use api\tests\_pages\AuthserverRoute;
|
||||
use api\tests\functional\_steps\AuthserverSteps;
|
||||
use Codeception\Example;
|
||||
|
||||
class SignoutCest {
|
||||
|
||||
/**
|
||||
* @var AuthserverRoute
|
||||
* @example {"login": "admin", "password": "password_0"}
|
||||
* @example {"login": "admin@ely.by", "password": "password_0"}
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(AuthserverSteps $I) {
|
||||
$this->route = new AuthserverRoute($I);
|
||||
}
|
||||
|
||||
public function byName(AuthserverSteps $I) {
|
||||
public function signout(AuthserverSteps $I, Example $example) {
|
||||
$I->wantTo('signout by nickname and password');
|
||||
$this->route->signout([
|
||||
'username' => 'admin',
|
||||
'password' => 'password_0',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
}
|
||||
|
||||
public function byEmail(AuthserverSteps $I) {
|
||||
$I->wantTo('signout by email and password');
|
||||
$this->route->signout([
|
||||
'username' => 'admin@ely.by',
|
||||
'password' => 'password_0',
|
||||
$I->sendPOST('/api/authserver/authentication/signout', [
|
||||
'username' => $example['login'],
|
||||
'password' => $example['password'],
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
@@ -37,7 +24,7 @@ class SignoutCest {
|
||||
|
||||
public function wrongArguments(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->signout([
|
||||
$I->sendPOST('/api/authserver/authentication/signout', [
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
@@ -50,7 +37,7 @@ class SignoutCest {
|
||||
|
||||
public function wrongNicknameAndPassword(AuthserverSteps $I) {
|
||||
$I->wantTo('signout by nickname and password with wrong data');
|
||||
$this->route->signout([
|
||||
$I->sendPOST('/api/authserver/authentication/signout', [
|
||||
'username' => 'nonexistent_user',
|
||||
'password' => 'nonexistent_password',
|
||||
]);
|
||||
@@ -64,7 +51,7 @@ class SignoutCest {
|
||||
|
||||
public function bannedAccount(AuthserverSteps $I) {
|
||||
$I->wantTo('signout from banned account');
|
||||
$this->route->signout([
|
||||
$I->sendPOST('/api/authserver/authentication/signout', [
|
||||
'username' => 'Banned',
|
||||
'password' => 'password_0',
|
||||
]);
|
||||
|
@@ -1,34 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\authserver;
|
||||
|
||||
use api\tests\_pages\AuthserverRoute;
|
||||
use api\tests\functional\_steps\AuthserverSteps;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class ValidateCest {
|
||||
|
||||
/**
|
||||
* @var AuthserverRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(AuthserverSteps $I) {
|
||||
$this->route = new AuthserverRoute($I);
|
||||
}
|
||||
|
||||
public function validate(AuthserverSteps $I) {
|
||||
$I->wantTo('validate my accessToken');
|
||||
[$accessToken] = $I->amAuthenticated();
|
||||
$this->route->validate([
|
||||
$I->sendPOST('/api/authserver/authentication/validate', [
|
||||
'accessToken' => $accessToken,
|
||||
]);
|
||||
$I->seeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
}
|
||||
|
||||
public function validateLegacyToken(AuthserverSteps $I) {
|
||||
$I->wantTo('validate my legacy accessToken');
|
||||
$I->sendPOST('/api/authserver/authentication/validate', [
|
||||
'accessToken' => 'e7bb6648-2183-4981-9b86-eba5e7f87b42',
|
||||
]);
|
||||
$I->seeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
}
|
||||
|
||||
public function wrongArguments(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->validate([
|
||||
$I->sendPOST('/api/authserver/authentication/validate', [
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
@@ -41,7 +42,7 @@ class ValidateCest {
|
||||
|
||||
public function wrongAccessToken(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong accessToken');
|
||||
$this->route->validate([
|
||||
$I->sendPOST('/api/authserver/authentication/validate', [
|
||||
'accessToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
@@ -54,9 +55,21 @@ class ValidateCest {
|
||||
|
||||
public function expiredAccessToken(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on expired accessToken');
|
||||
$this->route->validate([
|
||||
// Knowingly expired token from the dump
|
||||
'accessToken' => '6042634a-a1e2-4aed-866c-c661fe4e63e2',
|
||||
$I->sendPOST('/api/authserver/authentication/validate', [
|
||||
'accessToken' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE1NzU0Nzk1NTMsImV4cCI6MTU3NTQ3OTU1MywiZWx5LXNjb3BlcyI6Im1pbmVjcmFmdF9zZXJ2ZXJfc2Vzc2lvbiIsImVseS1jbGllbnQtdG9rZW4iOiJyZW1vdmVkIiwic3ViIjoiZWx5fDEifQ.xDMs5B48nH6p3a1k3WoZKtW4zoNHGGaLD1OGTFte-sUJb2fNMR65LuuBW8DzqO2odgco2xX660zqbhB-tp2OsA',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Token expired.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function expiredLegacyAccessToken(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on expired legacy accessToken');
|
||||
$I->sendPOST('/api/authserver/authentication/validate', [
|
||||
'accessToken' => '6042634a-a1e2-4aed-866c-c661fe4e63e2', // Already expired token from the fixtures
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
|
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\IdentityInfoRoute;
|
||||
use api\tests\functional\_steps\OauthSteps;
|
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
namespace api\tests\functional\oauth;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\dev\applications;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
@@ -1,113 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\oauth;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\functional\_steps\OauthSteps;
|
||||
use api\tests\FunctionalTester;
|
||||
|
||||
class AccessTokenCest {
|
||||
|
||||
/**
|
||||
* @var OauthRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new OauthRoute($I);
|
||||
public function successfullyIssueToken(OauthSteps $I) {
|
||||
$I->wantTo('complete oauth flow and obtain access_token');
|
||||
$authCode = $I->obtainAuthCode();
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $authCode,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
$I->cantSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
}
|
||||
|
||||
public function testIssueTokenWithWrongArgs(OauthSteps $I) {
|
||||
$I->wantTo('check behavior on on request without any credentials');
|
||||
$this->route->issueToken();
|
||||
public function successfullyIssueOfflineToken(OauthSteps $I) {
|
||||
$I->wantTo('complete oauth flow with offline_access scope and obtain access_token and refresh_token');
|
||||
$authCode = $I->obtainAuthCode(['offline_access']);
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $authCode,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
}
|
||||
|
||||
public function callEndpointWithByEmptyRequest(OauthSteps $I) {
|
||||
$I->wantTo('check behavior on on request without any params');
|
||||
$I->sendPOST('/api/oauth2/v1/token');
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_request',
|
||||
'message' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Check the "grant_type" parameter.',
|
||||
'error' => 'unsupported_grant_type',
|
||||
'message' => 'The authorization grant type is not supported by the authorization server.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function issueTokenByPassingInvalidAuthCode(OauthSteps $I) {
|
||||
$I->wantTo('check behavior on passing invalid auth code');
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'wrong-auth-code',
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'http://ely.by'
|
||||
));
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => 'wrong-auth-code',
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_request',
|
||||
'message' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Check the "code" parameter.',
|
||||
]);
|
||||
}
|
||||
|
||||
$authCode = $I->getAuthCode();
|
||||
public function issueTokenByPassingInvalidRedirectUri(OauthSteps $I) {
|
||||
$I->wantTo('check behavior on passing invalid redirect_uri');
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$authCode,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'http://some-other.domain'
|
||||
));
|
||||
$authCode = $I->obtainAuthCode();
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $authCode,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'redirect_uri' => 'http://some-other.domain',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_client',
|
||||
'message' => 'Client authentication failed.',
|
||||
'message' => 'Client authentication failed',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testIssueToken(OauthSteps $I) {
|
||||
$authCode = $I->getAuthCode();
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$authCode,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'http://ely.by'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->cantSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
}
|
||||
|
||||
public function testIssueTokenWithRefreshToken(OauthSteps $I) {
|
||||
$authCode = $I->getAuthCode(['offline_access']);
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$authCode,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'http://ely.by'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
}
|
||||
|
||||
private function buildParams($code = null, $clientId = null, $clientSecret = null, $redirectUri = null) {
|
||||
$params = ['grant_type' => 'authorization_code'];
|
||||
if ($code !== null) {
|
||||
$params['code'] = $code;
|
||||
}
|
||||
|
||||
if ($clientId !== null) {
|
||||
$params['client_id'] = $clientId;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null) {
|
||||
$params['client_secret'] = $clientSecret;
|
||||
}
|
||||
|
||||
if ($redirectUri !== null) {
|
||||
$params['redirect_uri'] = $redirectUri;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,155 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\oauth;
|
||||
|
||||
use api\rbac\Permissions as P;
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\FunctionalTester;
|
||||
|
||||
class AuthCodeCest {
|
||||
|
||||
/**
|
||||
* @var OauthRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new OauthRoute($I);
|
||||
}
|
||||
|
||||
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) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('validate all oAuth params on complete request');
|
||||
$this->testOauthParamsValidation($I, 'complete');
|
||||
}
|
||||
|
||||
public function testCompleteActionOnWrongConditions(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
|
||||
$I->wantTo('get accept_required if I don\'t require any scope, but this is first time request');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
|
||||
$I->wantTo('get accept_required if I require some scopes on first time');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
[P::MINECRAFT_SERVER_SESSION]
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCompleteActionSuccess(FunctionalTester $I) {
|
||||
public function completeSuccess(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('get auth code if I require some scope and pass accept field');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
[P::MINECRAFT_SERVER_SESSION]
|
||||
), ['accept' => true]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
|
||||
$I->wantTo('get auth code if I don\'t require any scope and don\'t pass accept field, but previously have ' .
|
||||
'successful request');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
|
||||
$I->wantTo('get auth code if I require some scopes and don\'t pass accept field, but previously have successful ' .
|
||||
'request with same scopes');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
[P::MINECRAFT_SERVER_SESSION]
|
||||
));
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session',
|
||||
]), ['accept' => true]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
@@ -157,26 +23,98 @@ class AuthCodeCest {
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
public function testAcceptRequiredOnNewScope(FunctionalTester $I) {
|
||||
/**
|
||||
* @before completeSuccess
|
||||
*/
|
||||
public function completeSuccessWithLessScopes(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('get accept_required if I have previous successful request, but now require some new scope');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
[P::MINECRAFT_SERVER_SESSION]
|
||||
), ['accept' => true]);
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
[P::MINECRAFT_SERVER_SESSION, 'account_info']
|
||||
));
|
||||
$I->wantTo('get auth code with less scopes as passed in the previous request without accept param');
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
]));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
/**
|
||||
* @before completeSuccess
|
||||
*/
|
||||
public function completeSuccessWithSameScopes(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('get auth code with the same scopes as passed in the previous request without accept param');
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session',
|
||||
]));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
public function acceptRequiredOnFirstAuthRequest1(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('get accept_required if I don\'t require any scope, but this is first time request');
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
]));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'parameter' => null,
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
|
||||
public function acceptRequiredOnFirstAuthRequest2(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('get accept_required if I require some scopes on first time');
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session',
|
||||
]));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => null,
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
|
||||
public function acceptRequiredOnNewScope(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('get accept_required if I have previous successful request, but now require some new scope');
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session',
|
||||
]), ['accept' => true]);
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session account_info',
|
||||
]));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => null,
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
@@ -184,72 +122,30 @@ class AuthCodeCest {
|
||||
public function testCompleteActionWithDismissState(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('get access_denied error if I pass accept in false state');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
[P::MINECRAFT_SERVER_SESSION]
|
||||
), ['accept' => false]);
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session',
|
||||
]), ['accept' => false]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'access_denied',
|
||||
'parameter' => '',
|
||||
'parameter' => null,
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
private function buildQueryParams(
|
||||
$clientId = null,
|
||||
$redirectUri = null,
|
||||
$responseType = null,
|
||||
$scopes = [],
|
||||
$state = null,
|
||||
$customData = []
|
||||
) {
|
||||
$params = $customData;
|
||||
if ($clientId !== null) {
|
||||
$params['client_id'] = $clientId;
|
||||
}
|
||||
|
||||
if ($redirectUri !== null) {
|
||||
$params['redirect_uri'] = $redirectUri;
|
||||
}
|
||||
|
||||
if ($responseType !== null) {
|
||||
$params['response_type'] = $responseType;
|
||||
}
|
||||
|
||||
if ($state !== null) {
|
||||
$params['state'] = $state;
|
||||
}
|
||||
|
||||
if (!empty($scopes)) {
|
||||
if (is_array($scopes)) {
|
||||
$scopes = implode(',', $scopes);
|
||||
}
|
||||
|
||||
$params['scope'] = $scopes;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function testOauthParamsValidation(FunctionalTester $I, $action) {
|
||||
$I->wantTo('check behavior on invalid request without one or few params');
|
||||
$this->route->$action($this->buildQueryParams());
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'invalid_request',
|
||||
'parameter' => 'client_id',
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
|
||||
public function invalidClientId(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('check behavior on invalid client id');
|
||||
$this->route->$action($this->buildQueryParams('non-exists-client', 'http://some-resource.by', 'code'));
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'non-exists-client',
|
||||
'redirect_uri' => 'http://some-resource.by',
|
||||
'response_type' => 'code',
|
||||
]));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
@@ -257,23 +153,16 @@ class AuthCodeCest {
|
||||
'error' => 'invalid_client',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
|
||||
$I->wantTo('check behavior on invalid response type');
|
||||
$this->route->$action($this->buildQueryParams('ely', 'http://ely.by', 'kitty'));
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'unsupported_response_type',
|
||||
'parameter' => 'kitty',
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
|
||||
public function invalidScopes(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('check behavior on some invalid scopes');
|
||||
$this->route->$action($this->buildQueryParams('ely', 'http://ely.by', 'code', [
|
||||
P::MINECRAFT_SERVER_SESSION,
|
||||
'some_wrong_scope',
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session some_wrong_scope',
|
||||
]));
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
@@ -284,18 +173,23 @@ class AuthCodeCest {
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
public function requestInternalScope(FunctionalTester $I) {
|
||||
$I->amAuthenticated();
|
||||
$I->wantTo('check behavior on request internal scope');
|
||||
$this->route->$action($this->buildQueryParams('ely', 'http://ely.by', 'code', [
|
||||
P::MINECRAFT_SERVER_SESSION,
|
||||
P::BLOCK_ACCOUNT,
|
||||
]));
|
||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session block_account',
|
||||
]), ['accept' => true]); // TODO: maybe remove?
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'invalid_scope',
|
||||
'parameter' => P::BLOCK_ACCOUNT,
|
||||
'parameter' => 'block_account',
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
|
@@ -1,120 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\oauth;
|
||||
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\functional\_steps\OauthSteps;
|
||||
use api\tests\FunctionalTester;
|
||||
|
||||
class ClientCredentialsCest {
|
||||
|
||||
/**
|
||||
* @var OauthRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new OauthRoute($I);
|
||||
public function issueTokenWithPublicScopes(FunctionalTester $I) {
|
||||
$I->wantTo('issue token as not trusted client and require only public scopes');
|
||||
// We don't have any public scopes yet for this grant, so the test runs with an empty set
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'scope' => '',
|
||||
]);
|
||||
$this->assertSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function testIssueTokenWithWrongArgs(FunctionalTester $I) {
|
||||
$I->wantTo('check behavior on on request without any credentials');
|
||||
$this->route->issueToken($this->buildParams());
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_request',
|
||||
public function issueTokenWithInternalScopesAsNotTrustedClient(FunctionalTester $I) {
|
||||
$I->wantTo('issue token as not trusted client and require some internal scope');
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'scope' => 'block_account',
|
||||
]);
|
||||
|
||||
$I->wantTo('check behavior on passing invalid client_id');
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'invalid-client',
|
||||
'invalid-secret',
|
||||
['invalid-scope']
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_client',
|
||||
]);
|
||||
|
||||
$I->wantTo('check behavior on passing invalid client_secret');
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'ely',
|
||||
'invalid-secret',
|
||||
['invalid-scope']
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_client',
|
||||
]);
|
||||
|
||||
$I->wantTo('check behavior on passing invalid client_secret');
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'ely',
|
||||
'invalid-secret',
|
||||
['invalid-scope']
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_client',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testIssueTokenWithPublicScopes(OauthSteps $I) {
|
||||
// TODO: we don't have any public scopes yet for this grant, so the test runs with an empty set
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
[]
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
}
|
||||
|
||||
public function testIssueTokenWithInternalScopes(OauthSteps $I) {
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
['account_block']
|
||||
));
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_scope',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'trusted-client',
|
||||
'tXBbyvMcyaOgHMOAXBpN2EC7uFoJAaL9',
|
||||
['account_block']
|
||||
));
|
||||
public function issueTokenWithInternalScopesAsTrustedClient(FunctionalTester $I) {
|
||||
$I->wantTo('issue token as trusted client and require some internal scope');
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => 'trusted-client',
|
||||
'client_secret' => 'tXBbyvMcyaOgHMOAXBpN2EC7uFoJAaL9',
|
||||
'scope' => 'block_account',
|
||||
]);
|
||||
$this->assertSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function issueTokenByPassingInvalidClientId(FunctionalTester $I) {
|
||||
$I->wantToTest('behavior on passing invalid client_id');
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => 'invalid-client',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'scope' => 'block_account',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_client',
|
||||
]);
|
||||
}
|
||||
|
||||
public function issueTokenByPassingInvalidClientSecret(FunctionalTester $I) {
|
||||
$I->wantTo('check behavior on passing invalid client_secret');
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'client_credentials',
|
||||
'client_id' => 'trusted-client',
|
||||
'client_secret' => 'invalid-secret',
|
||||
'scope' => 'block_account',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_client',
|
||||
]);
|
||||
}
|
||||
|
||||
private function assertSuccessResponse(FunctionalTester $I): void {
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||
}
|
||||
|
||||
private function buildParams($clientId = null, $clientSecret = null, array $scopes = null) {
|
||||
$params = ['grant_type' => 'client_credentials'];
|
||||
if ($clientId !== null) {
|
||||
$params['client_id'] = $clientId;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null) {
|
||||
$params['client_secret'] = $clientSecret;
|
||||
}
|
||||
|
||||
if ($scopes !== null) {
|
||||
$params['scope'] = implode(',', $scopes);
|
||||
}
|
||||
|
||||
return $params;
|
||||
$I->cantSeeResponseJsonMatchesJsonPath('$.refresh_token');
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,83 +1,96 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\oauth;
|
||||
|
||||
use api\components\OAuth2\Storage\ScopeStorage as S;
|
||||
use api\rbac\Permissions as P;
|
||||
use api\tests\_pages\OauthRoute;
|
||||
use api\tests\functional\_steps\OauthSteps;
|
||||
use api\tests\FunctionalTester;
|
||||
|
||||
class RefreshTokenCest {
|
||||
|
||||
/**
|
||||
* @var OauthRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new OauthRoute($I);
|
||||
public function refreshToken(OauthSteps $I) {
|
||||
$I->wantTo('refresh token without passing the desired scopes');
|
||||
$refreshToken = $I->getRefreshToken();
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
]);
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
}
|
||||
|
||||
public function testInvalidRefreshToken(OauthSteps $I) {
|
||||
$this->route->issueToken($this->buildParams(
|
||||
'some-invalid-refresh-token',
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM'
|
||||
));
|
||||
public function refreshTokenWithSameScopes(OauthSteps $I) {
|
||||
$refreshToken = $I->getRefreshToken(['minecraft_server_session']);
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'scope' => 'minecraft_server_session',
|
||||
]);
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
}
|
||||
|
||||
public function refreshTokenTwice(OauthSteps $I) {
|
||||
$I->wantTo('refresh token two times in a row and ensure, that token isn\'t rotating');
|
||||
$refreshToken = $I->getRefreshToken(['minecraft_server_session']);
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'scope' => 'minecraft_server_session',
|
||||
]);
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'scope' => 'minecraft_server_session',
|
||||
]);
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
}
|
||||
|
||||
public function refreshTokenUsingLegacyToken(FunctionalTester $I) {
|
||||
$I->wantTo('refresh token using the legacy token');
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => 'op7kPGAgHlsXRBJtkFg7wKOTpodvtHVW5NxR7Tjr',
|
||||
'client_id' => 'test1',
|
||||
'client_secret' => 'eEvrKHF47sqiaX94HsX-xXzdGiz3mcsq',
|
||||
'scope' => 'minecraft_server_session account_info',
|
||||
]);
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
}
|
||||
|
||||
public function passInvalidRefreshToken(OauthSteps $I) {
|
||||
$I->wantToTest('behaviour of the server when invalid refresh token passed');
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => 'some-invalid-refresh-token',
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'invalid_request',
|
||||
'message' => 'The refresh token is invalid.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testRefreshToken(OauthSteps $I) {
|
||||
$refreshToken = $I->getRefreshToken();
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$refreshToken,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM'
|
||||
));
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
}
|
||||
|
||||
public function testRefreshTokenWithSameScopes(OauthSteps $I) {
|
||||
$refreshToken = $I->getRefreshToken([P::MINECRAFT_SERVER_SESSION]);
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$refreshToken,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
[P::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
|
||||
));
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
}
|
||||
|
||||
public function testRefreshTokenTwice(OauthSteps $I) {
|
||||
$refreshToken = $I->getRefreshToken([P::MINECRAFT_SERVER_SESSION]);
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$refreshToken,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
[P::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
|
||||
));
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$refreshToken,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
[P::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
|
||||
));
|
||||
$this->canSeeRefreshTokenSuccess($I);
|
||||
}
|
||||
|
||||
public function testRefreshTokenWithNewScopes(OauthSteps $I) {
|
||||
$refreshToken = $I->getRefreshToken([P::MINECRAFT_SERVER_SESSION]);
|
||||
$this->route->issueToken($this->buildParams(
|
||||
$refreshToken,
|
||||
'ely',
|
||||
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
[P::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS, 'account_email']
|
||||
));
|
||||
public function requireNewScopes(OauthSteps $I) {
|
||||
$I->wantToTest('behavior when required the new scope that was not issued with original token');
|
||||
$refreshToken = $I->getRefreshToken(['minecraft_server_session']);
|
||||
$I->sendPOST('/api/oauth2/v1/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
'client_id' => 'ely',
|
||||
'client_secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'scope' => 'minecraft_server_session account_email',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
@@ -85,34 +98,8 @@ class RefreshTokenCest {
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildParams($refreshToken = null, $clientId = null, $clientSecret = null, $scopes = []) {
|
||||
$params = ['grant_type' => 'refresh_token'];
|
||||
if ($refreshToken !== null) {
|
||||
$params['refresh_token'] = $refreshToken;
|
||||
}
|
||||
|
||||
if ($clientId !== null) {
|
||||
$params['client_id'] = $clientId;
|
||||
}
|
||||
|
||||
if ($clientSecret !== null) {
|
||||
$params['client_secret'] = $clientSecret;
|
||||
}
|
||||
|
||||
if (!empty($scopes)) {
|
||||
if (is_array($scopes)) {
|
||||
$scopes = implode(',', $scopes);
|
||||
}
|
||||
|
||||
$params['scope'] = $scopes;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function canSeeRefreshTokenSuccess(OauthSteps $I) {
|
||||
private function canSeeRefreshTokenSuccess(FunctionalTester $I) {
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'token_type' => 'Bearer',
|
||||
]);
|
||||
|
113
api/tests/functional/oauth/ValidateCest.php
Normal file
113
api/tests/functional/oauth/ValidateCest.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\functional\oauth;
|
||||
|
||||
use api\tests\FunctionalTester;
|
||||
|
||||
class ValidateCest {
|
||||
|
||||
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',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function unknownClientId(FunctionalTester $I) {
|
||||
$I->wantTo('check behavior on invalid client id');
|
||||
$I->sendGET('/api/oauth2/v1/validate', [
|
||||
'client_id' => 'non-exists-client',
|
||||
'redirect_uri' => 'http://some-resource.by',
|
||||
'response_type' => 'code',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'invalid_client',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
|
||||
public function invalidScopes(FunctionalTester $I) {
|
||||
$I->wantTo('check behavior on some invalid scopes');
|
||||
$I->sendGET('/api/oauth2/v1/validate', [
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session some_wrong_scope',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'invalid_scope',
|
||||
'parameter' => 'some_wrong_scope',
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
public function requestInternalScope(FunctionalTester $I) {
|
||||
$I->wantTo('check behavior on request internal scope');
|
||||
$I->sendGET('/api/oauth2/v1/validate', [
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session block_account',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'invalid_scope',
|
||||
'parameter' => 'block_account',
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\OAuth2\Entities;
|
||||
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\tests\unit\TestCase;
|
||||
use DateTimeImmutable;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
|
||||
class AccessTokenEntityTest extends TestCase {
|
||||
|
||||
public function testToString() {
|
||||
/** @var ClientEntityInterface|\PHPUnit\Framework\MockObject\MockObject $client */
|
||||
$client = $this->createMock(ClientEntityInterface::class);
|
||||
$client->method('getIdentifier')->willReturn('mockClientId');
|
||||
|
||||
$entity = new AccessTokenEntity();
|
||||
$entity->setClient($client);
|
||||
$entity->setExpiryDateTime(new DateTimeImmutable());
|
||||
$entity->addScope($this->createScopeEntity('first'));
|
||||
$entity->addScope($this->createScopeEntity('second'));
|
||||
|
||||
$token = (string)$entity;
|
||||
$payloads = json_decode(base64_decode(explode('.', $token)[1]), true);
|
||||
$this->assertSame('first,second', $payloads['ely-scopes']);
|
||||
}
|
||||
|
||||
private function createScopeEntity(string $id): ScopeEntityInterface {
|
||||
/** @var ScopeEntityInterface|\PHPUnit\Framework\MockObject\MockObject $entity */
|
||||
$entity = $this->createMock(ScopeEntityInterface::class);
|
||||
$entity->method('getIdentifier')->willReturn($id);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
@@ -22,7 +22,11 @@ class ComponentTest extends TestCase {
|
||||
$this->assertSame('ES256', $token->getHeader('alg'));
|
||||
$this->assertEmpty(array_diff(array_keys($token->getClaims()), ['iat', 'exp']));
|
||||
$this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 1);
|
||||
$this->assertEqualsWithDelta(time() + 3600, $token->getClaim('exp'), 2);
|
||||
|
||||
// Pass exp claim
|
||||
$time = time() + 60;
|
||||
$token = $this->component->create(['exp' => $time]);
|
||||
$this->assertSame($time, $token->getClaim('exp'));
|
||||
|
||||
// Pass custom payloads
|
||||
$token = $this->component->create(['find' => 'me']);
|
||||
|
@@ -5,16 +5,24 @@ namespace api\tests\unit\components\Tokens;
|
||||
|
||||
use api\components\Tokens\TokensFactory;
|
||||
use api\tests\unit\TestCase;
|
||||
use Carbon\Carbon;
|
||||
use common\models\Account;
|
||||
use common\models\AccountSession;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
|
||||
class TokensFactoryTest extends TestCase {
|
||||
|
||||
public function testCreateForAccount() {
|
||||
$factory = new TokensFactory();
|
||||
|
||||
$account = new Account();
|
||||
$account->id = 1;
|
||||
|
||||
$token = TokensFactory::createForAccount($account);
|
||||
// Create for account
|
||||
|
||||
$token = $factory->createForWebAccount($account);
|
||||
$this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 1);
|
||||
$this->assertEqualsWithDelta(time() + 60 * 60 * 24 * 7, $token->getClaim('exp'), 2);
|
||||
$this->assertSame('ely|1', $token->getClaim('sub'));
|
||||
@@ -24,7 +32,9 @@ class TokensFactoryTest extends TestCase {
|
||||
$session = new AccountSession();
|
||||
$session->id = 2;
|
||||
|
||||
$token = TokensFactory::createForAccount($account, $session);
|
||||
// Create for account with remember me
|
||||
|
||||
$token = $factory->createForWebAccount($account, $session);
|
||||
$this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 1);
|
||||
$this->assertEqualsWithDelta(time() + 3600, $token->getClaim('exp'), 2);
|
||||
$this->assertSame('ely|1', $token->getClaim('sub'));
|
||||
@@ -32,4 +42,60 @@ class TokensFactoryTest extends TestCase {
|
||||
$this->assertSame(2, $token->getClaim('jti'));
|
||||
}
|
||||
|
||||
public function testCreateForOauthClient() {
|
||||
$factory = new TokensFactory();
|
||||
|
||||
$client = $this->createMock(ClientEntityInterface::class);
|
||||
$client->method('getIdentifier')->willReturn('clientId');
|
||||
|
||||
$scope1 = $this->createMock(ScopeEntityInterface::class);
|
||||
$scope1->method('getIdentifier')->willReturn('scope1');
|
||||
$scope2 = $this->createMock(ScopeEntityInterface::class);
|
||||
$scope2->method('getIdentifier')->willReturn('scope2');
|
||||
|
||||
$expiryDateTime = Carbon::now()->addDay();
|
||||
|
||||
// Create for auth code grant
|
||||
|
||||
$accessToken = $this->createMock(AccessTokenEntityInterface::class);
|
||||
$accessToken->method('getClient')->willReturn($client);
|
||||
$accessToken->method('getScopes')->willReturn([$scope1, $scope2]);
|
||||
$accessToken->method('getExpiryDateTime')->willReturn($expiryDateTime);
|
||||
$accessToken->method('getUserIdentifier')->willReturn(1);
|
||||
|
||||
$token = $factory->createForOAuthClient($accessToken);
|
||||
$this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 1);
|
||||
$this->assertEqualsWithDelta($expiryDateTime->getTimestamp(), $token->getClaim('exp'), 2);
|
||||
$this->assertSame('ely|1', $token->getClaim('sub'));
|
||||
$this->assertSame('client|clientId', $token->getClaim('aud'));
|
||||
$this->assertSame('scope1,scope2', $token->getClaim('ely-scopes'));
|
||||
|
||||
// Create for client credentials grant
|
||||
|
||||
$accessToken = $this->createMock(AccessTokenEntityInterface::class);
|
||||
$accessToken->method('getClient')->willReturn($client);
|
||||
$accessToken->method('getScopes')->willReturn([$scope1, $scope2]);
|
||||
$accessToken->method('getExpiryDateTime')->willReturn(Carbon::now()->subDay());
|
||||
$accessToken->method('getUserIdentifier')->willReturn(null);
|
||||
|
||||
$token = $factory->createForOAuthClient($accessToken);
|
||||
$this->assertSame('no value', $token->getClaim('exp', 'no value'));
|
||||
$this->assertSame('no value', $token->getClaim('sub', 'no value'));
|
||||
}
|
||||
|
||||
public function testCreateForMinecraftAccount() {
|
||||
$factory = new TokensFactory();
|
||||
|
||||
$account = new Account();
|
||||
$account->id = 1;
|
||||
$clientToken = 'e44fae79-f80e-4975-952e-47e8a9ed9472';
|
||||
|
||||
$token = $factory->createForMinecraftAccount($account, $clientToken);
|
||||
$this->assertEqualsWithDelta(time(), $token->getClaim('iat'), 5);
|
||||
$this->assertEqualsWithDelta(time() + 60 * 60 * 24 * 2, $token->getClaim('exp'), 5);
|
||||
$this->assertSame('minecraft_server_session', $token->getClaim('ely-scopes'));
|
||||
$this->assertNotSame('e44fae79-f80e-4975-952e-47e8a9ed9472', $token->getClaim('ely-client-token'));
|
||||
$this->assertSame('ely|1', $token->getClaim('sub'));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,13 +5,16 @@ namespace codeception\api\unit\components\User;
|
||||
|
||||
use api\components\User\Component;
|
||||
use api\components\User\JwtIdentity;
|
||||
use api\components\User\OAuth2Identity;
|
||||
use api\components\User\LegacyOAuth2Identity;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\Account;
|
||||
use common\models\AccountSession;
|
||||
use common\models\OauthClient;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
use common\tests\fixtures\AccountSessionFixture;
|
||||
use common\tests\fixtures\MinecraftAccessKeyFixture;
|
||||
use common\tests\fixtures\OauthClientFixture;
|
||||
use common\tests\fixtures\OauthSessionFixture;
|
||||
use Lcobucci\JWT\Claim\Basic;
|
||||
use Lcobucci\JWT\Token;
|
||||
|
||||
@@ -32,6 +35,8 @@ class ComponentTest extends TestCase {
|
||||
'accounts' => AccountFixture::class,
|
||||
'sessions' => AccountSessionFixture::class,
|
||||
'minecraftSessions' => MinecraftAccessKeyFixture::class,
|
||||
'oauthClients' => OauthClientFixture::class,
|
||||
'oauthSessions' => OauthSessionFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -41,7 +46,7 @@ class ComponentTest extends TestCase {
|
||||
$this->assertNull($component->getActiveSession());
|
||||
|
||||
// Identity is a Oauth2Identity
|
||||
$component->setIdentity(mock(OAuth2Identity::class));
|
||||
$component->setIdentity(mock(LegacyOAuth2Identity::class));
|
||||
$this->assertNull($component->getActiveSession());
|
||||
|
||||
// Identity is correct, but have no jti claim
|
||||
@@ -88,6 +93,7 @@ class ComponentTest extends TestCase {
|
||||
$component->terminateSessions($account, Component::KEEP_SITE_SESSIONS);
|
||||
$this->assertEmpty($account->getMinecraftAccessKeys()->all());
|
||||
$this->assertNotEmpty($account->getSessions()->all());
|
||||
$this->assertEqualsWithDelta(time(), $account->getOauthSessions()->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])->one()->revoked_at, 5);
|
||||
|
||||
// All sessions should be removed except the current one
|
||||
$component->terminateSessions($account, Component::KEEP_CURRENT_SESSION);
|
||||
|
@@ -3,41 +3,31 @@ declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\User;
|
||||
|
||||
use api\components\OAuth2\Component;
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\components\User\IdentityFactory;
|
||||
use api\components\User\JwtIdentity;
|
||||
use api\components\User\OAuth2Identity;
|
||||
use api\components\User\LegacyOAuth2Identity;
|
||||
use api\tests\unit\TestCase;
|
||||
use Carbon\Carbon;
|
||||
use League\OAuth2\Server\AbstractServer;
|
||||
use League\OAuth2\Server\Storage\AccessTokenInterface;
|
||||
use Yii;
|
||||
use common\tests\fixtures;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class IdentityFactoryTest extends TestCase {
|
||||
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
fixtures\LegacyOauthAccessTokenFixture::class,
|
||||
fixtures\LegacyOauthAccessTokenScopeFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function testFindIdentityByAccessToken() {
|
||||
// Find identity by jwt token
|
||||
// Find identity by the JWT
|
||||
$identity = IdentityFactory::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.4Oidvuo4spvUf9hkpHR72eeqZUh2Zbxh_L8Od3vcgTj--0iOrcOEp6zwmEW6vF7BTHtjz2b3mXce61bqsCjXjQ');
|
||||
$this->assertInstanceOf(JwtIdentity::class, $identity);
|
||||
|
||||
// Find identity by oauth2 token
|
||||
$accessToken = new AccessTokenEntity(mock(AbstractServer::class));
|
||||
$accessToken->setExpireTime(time() + 3600);
|
||||
$accessToken->setId('mock-token');
|
||||
|
||||
/** @var AccessTokenInterface|\Mockery\MockInterface $accessTokensStorage */
|
||||
$accessTokensStorage = mock(AccessTokenInterface::class);
|
||||
$accessTokensStorage->shouldReceive('get')->with('mock-token')->andReturn($accessToken);
|
||||
|
||||
/** @var Component|\Mockery\MockInterface $component */
|
||||
$component = mock(Component::class);
|
||||
$component->shouldReceive('getAccessTokenStorage')->andReturn($accessTokensStorage);
|
||||
Yii::$app->set('oauth', $component);
|
||||
|
||||
$identity = IdentityFactory::findIdentityByAccessToken('mock-token');
|
||||
$this->assertInstanceOf(OAuth2Identity::class, $identity);
|
||||
// Find identity by the legacy OAuth2 token
|
||||
$identity = IdentityFactory::findIdentityByAccessToken('ZZQP8sS9urzriy8N9h6FwFNMOH3PkZ5T5PLqS6SX');
|
||||
$this->assertInstanceOf(LegacyOAuth2Identity::class, $identity);
|
||||
}
|
||||
|
||||
public function testFindIdentityByAccessTokenWithEmptyValue() {
|
||||
|
@@ -7,6 +7,8 @@ use api\components\User\JwtIdentity;
|
||||
use api\tests\unit\TestCase;
|
||||
use Carbon\Carbon;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
use common\tests\fixtures\OauthClientFixture;
|
||||
use common\tests\fixtures\OauthSessionFixture;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class JwtIdentityTest extends TestCase {
|
||||
@@ -14,6 +16,8 @@ class JwtIdentityTest extends TestCase {
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
'accounts' => AccountFixture::class,
|
||||
'oauthClients' => OauthClientFixture::class,
|
||||
'oauthSessions' => OauthSessionFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -46,14 +50,18 @@ class JwtIdentityTest extends TestCase {
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTc3NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ._6hj6XUSmSLibgT9ZE1Pokf4oI9r-d6tEc1z2J-fBlr1710Qiso5yNcXqb3Z_xy7Qtemyq8jOlOZA8DvmkVBrg',
|
||||
'Incorrect token',
|
||||
];
|
||||
yield 'revoked by oauth client' => [
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudF9pbmZvLG1pbmVjcmFmdF9zZXJ2ZXJfc2Vzc2lvbiIsImlhdCI6MTU2NDYxMDUwMCwic3ViIjoiZWx5fDEiLCJhdWQiOiJjbGllbnR8dGxhdW5jaGVyIn0.YzUzvnREEoQPu8CvU6WLdysUU0bC_xzigQPs2LK1su38uysSYgSbPzNOZYkQnvcmVLehHY-ON44x-oA8Os-9ZA',
|
||||
'Token has been revoked',
|
||||
];
|
||||
yield 'revoked by unauthorized minecraft launcher' => [
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoibWluZWNyYWZ0X3NlcnZlcl9zZXNzaW9uIiwiZWx5LWNsaWVudC10b2tlbiI6IllBTVhneTBBcEI5Z2dUX1VYNjNJaTdKcGtNd2ZwTmxaaE8yVVVEeEZ3YTFmZ2g4dksyN0RtV25vN2xqbk1pWWJwQ1VuS09YVnR2V1YtVVg1dWRQVVFsLU4xY3BBZlJBX2EtZW1BZyIsImlhdCI6MTU2NDYxMDUwMCwic3ViIjoiZWx5fDEifQ.LtE9cQJ4z5dGVkDZl50M2HZH6kOYHgGz2RIycS_lzU9YLhosQ3ux7i2KI7qGI7BNuxO5zJ1OkxF2r9Qc240EpA',
|
||||
'Token has been revoked',
|
||||
];
|
||||
yield 'invalid signature' => [
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDEifQ.yth31f2PyhUkYSfBlizzUXWIgOvxxk8gNP-js0z8g1OT5rig40FPTIkgsZRctAwAAlj6QoIWW7-hxLTcSb2vmw',
|
||||
'Incorrect token',
|
||||
];
|
||||
yield 'invalid sub' => [
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoxMjM0fQ.yigP5nWFdX0ktbuZC_Unb9bWxpAVd7Nv8Fb1Vsa0t5WkVA88VbhPi2P-CenbDOy8ngwoGV9m3c3upMs2V3gqvw',
|
||||
'Incorrect token',
|
||||
];
|
||||
yield 'empty token' => ['', 'Incorrect token'];
|
||||
}
|
||||
|
||||
@@ -66,6 +74,10 @@ class JwtIdentityTest extends TestCase {
|
||||
$identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoiZWx5fDk5OTk5In0.1pAnhkR-_ZqzjLBR-PNIMJUXRSUK3aYixrFNKZg2ynPNPiDvzh8U-iBTT6XRfMP5nvfXZucRpoPVoiXtx40CUQ');
|
||||
$this->assertNull($identity->getAccount());
|
||||
|
||||
// Sub contains invalid value
|
||||
$identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Miwic3ViIjoxMjM0fQ.yigP5nWFdX0ktbuZC_Unb9bWxpAVd7Nv8Fb1Vsa0t5WkVA88VbhPi2P-CenbDOy8ngwoGV9m3c3upMs2V3gqvw');
|
||||
$this->assertNull($identity->getAccount());
|
||||
|
||||
// Token without sub claim
|
||||
$identity = JwtIdentity::findIdentityByAccessToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlbHktc2NvcGVzIjoiYWNjb3VudHNfd2ViX3VzZXIiLCJpYXQiOjE1NjQ2MTA1NDIsImV4cCI6MTU2NDYxNDE0Mn0.QxmYgSflZOQmhzYRr8bowU767yu4yKgTVaho0MPuyCmUfZO_0O0SQASMKVILf-wlT0ODTTG7vD753a2MTAmPmw');
|
||||
$this->assertNull($identity->getAccount());
|
||||
|
39
api/tests/unit/components/User/LegacyOAuth2IdentityTest.php
Normal file
39
api/tests/unit/components/User/LegacyOAuth2IdentityTest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\User;
|
||||
|
||||
use api\components\User\LegacyOAuth2Identity;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\tests\fixtures;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class LegacyOAuth2IdentityTest extends TestCase {
|
||||
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
fixtures\LegacyOauthAccessTokenFixture::class,
|
||||
fixtures\LegacyOauthAccessTokenScopeFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function testFindIdentityByAccessToken() {
|
||||
$identity = LegacyOAuth2Identity::findIdentityByAccessToken('ZZQP8sS9urzriy8N9h6FwFNMOH3PkZ5T5PLqS6SX');
|
||||
$this->assertSame('ZZQP8sS9urzriy8N9h6FwFNMOH3PkZ5T5PLqS6SX', $identity->getId());
|
||||
}
|
||||
|
||||
public function testFindIdentityByAccessTokenWithNonExistsToken() {
|
||||
$this->expectException(UnauthorizedHttpException::class);
|
||||
$this->expectExceptionMessage('Incorrect token');
|
||||
|
||||
LegacyOAuth2Identity::findIdentityByAccessToken('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
|
||||
}
|
||||
|
||||
public function testFindIdentityByAccessTokenWithExpiredToken() {
|
||||
$this->expectException(UnauthorizedHttpException::class);
|
||||
$this->expectExceptionMessage('Token expired');
|
||||
|
||||
LegacyOAuth2Identity::findIdentityByAccessToken('rc0sOF1SLdOxuD3bJcCQENmGTeYrGgy12qJScMx4');
|
||||
}
|
||||
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace api\tests\unit\components\User;
|
||||
|
||||
use api\components\OAuth2\Component;
|
||||
use api\components\OAuth2\Entities\AccessTokenEntity;
|
||||
use api\components\User\OAuth2Identity;
|
||||
use api\tests\unit\TestCase;
|
||||
use League\OAuth2\Server\AbstractServer;
|
||||
use League\OAuth2\Server\Storage\AccessTokenInterface;
|
||||
use Yii;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class OAuth2IdentityTest extends TestCase {
|
||||
|
||||
public function testFindIdentityByAccessToken() {
|
||||
$accessToken = new AccessTokenEntity(mock(AbstractServer::class));
|
||||
$accessToken->setExpireTime(time() + 3600);
|
||||
$accessToken->setId('mock-token');
|
||||
$this->mockFoundedAccessToken($accessToken);
|
||||
|
||||
$identity = OAuth2Identity::findIdentityByAccessToken('mock-token');
|
||||
$this->assertSame('mock-token', $identity->getId());
|
||||
}
|
||||
|
||||
public function testFindIdentityByAccessTokenWithNonExistsToken() {
|
||||
$this->expectException(UnauthorizedHttpException::class);
|
||||
$this->expectExceptionMessage('Incorrect token');
|
||||
|
||||
OAuth2Identity::findIdentityByAccessToken('not exists token');
|
||||
}
|
||||
|
||||
public function testFindIdentityByAccessTokenWithExpiredToken() {
|
||||
$this->expectException(UnauthorizedHttpException::class);
|
||||
$this->expectExceptionMessage('Token expired');
|
||||
|
||||
$accessToken = new AccessTokenEntity(mock(AbstractServer::class));
|
||||
$accessToken->setExpireTime(time() - 3600);
|
||||
$this->mockFoundedAccessToken($accessToken);
|
||||
|
||||
OAuth2Identity::findIdentityByAccessToken('mock-token');
|
||||
}
|
||||
|
||||
private function mockFoundedAccessToken(AccessTokenEntity $accessToken) {
|
||||
/** @var AccessTokenInterface|\Mockery\MockInterface $accessTokensStorage */
|
||||
$accessTokensStorage = mock(AccessTokenInterface::class);
|
||||
$accessTokensStorage->shouldReceive('get')->with('mock-token')->andReturn($accessToken);
|
||||
|
||||
/** @var Component|\Mockery\MockInterface $component */
|
||||
$component = mock(Component::class);
|
||||
$component->shouldReceive('getAccessTokenStorage')->andReturn($accessTokensStorage);
|
||||
Yii::$app->set('oauth', $component);
|
||||
}
|
||||
|
||||
}
|
@@ -21,19 +21,20 @@ class AuthenticationResultTest extends TestCase {
|
||||
}
|
||||
|
||||
public function testGetAsResponse() {
|
||||
$token = Yii::$app->tokens->create();
|
||||
$time = time() + 3600;
|
||||
$token = Yii::$app->tokens->create(['exp' => $time]);
|
||||
$jwt = (string)$token;
|
||||
|
||||
$model = new AuthenticationResult($token);
|
||||
$result = $model->formatAsOAuth2Response();
|
||||
$this->assertSame($jwt, $result['access_token']);
|
||||
$this->assertEqualsWithDelta(3600, $result['expires_in'], 1);
|
||||
$this->assertSame(3600, $result['expires_in']);
|
||||
$this->assertArrayNotHasKey('refresh_token', $result);
|
||||
|
||||
$model = new AuthenticationResult($token, 'refresh_token');
|
||||
$result = $model->formatAsOAuth2Response();
|
||||
$this->assertSame($jwt, $result['access_token']);
|
||||
$this->assertEqualsWithDelta(3600, $result['expires_in'], 1);
|
||||
$this->assertSame(3600, $result['expires_in']);
|
||||
$this->assertSame('refresh_token', $result['refresh_token']);
|
||||
}
|
||||
|
||||
|
@@ -131,6 +131,7 @@ class LoginFormTest extends TestCase {
|
||||
'login' => 'erickskrauch',
|
||||
'password' => '12345678',
|
||||
'account' => new Account([
|
||||
'id' => 1,
|
||||
'username' => 'erickskrauch',
|
||||
'password' => '12345678',
|
||||
'status' => Account::STATUS_ACTIVE,
|
||||
|
@@ -5,14 +5,12 @@ namespace codeception\api\unit\models\authentication;
|
||||
|
||||
use api\models\authentication\RefreshTokenForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use Codeception\Specify;
|
||||
use common\models\AccountSession;
|
||||
use common\tests\fixtures\AccountSessionFixture;
|
||||
use Yii;
|
||||
use yii\web\Request;
|
||||
|
||||
class RefreshTokenFormTest extends TestCase {
|
||||
use Specify;
|
||||
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
@@ -21,9 +19,8 @@ class RefreshTokenFormTest extends TestCase {
|
||||
}
|
||||
|
||||
public function testRenew() {
|
||||
/** @var Request|\Mockery\MockInterface $request */
|
||||
$request = mock(Request::class . '[getUserIP]')->makePartial();
|
||||
$request->shouldReceive('getUserIP')->andReturn('10.1.2.3');
|
||||
$request = $this->createPartialMock(Request::class, ['getUserIP']);
|
||||
$request->method('getUserIP')->willReturn('10.1.2.3');
|
||||
Yii::$app->set('request', $request);
|
||||
|
||||
$model = new RefreshTokenForm();
|
||||
|
@@ -3,139 +3,60 @@ declare(strict_types=1);
|
||||
|
||||
namespace codeception\api\unit\modules\authserver\models;
|
||||
|
||||
use api\models\authentication\LoginForm;
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\models\AuthenticateData;
|
||||
use api\modules\authserver\models\AuthenticationForm;
|
||||
use api\tests\unit\TestCase;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
use common\tests\_support\ProtectedCaller;
|
||||
use common\models\OauthClient;
|
||||
use common\models\OauthSession;
|
||||
use common\tests\fixtures\AccountFixture;
|
||||
use common\tests\fixtures\MinecraftAccessKeyFixture;
|
||||
use common\tests\fixtures\OauthClientFixture;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class AuthenticationFormTest extends TestCase {
|
||||
use ProtectedCaller;
|
||||
|
||||
public function _fixtures(): array {
|
||||
return [
|
||||
'accounts' => AccountFixture::class,
|
||||
'minecraftAccessKeys' => MinecraftAccessKeyFixture::class,
|
||||
'oauthClients' => OauthClientFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function testAuthenticateByWrongNicknamePass() {
|
||||
$this->expectException(ForbiddenOperationException::class);
|
||||
$this->expectExceptionMessage('Invalid credentials. Invalid nickname or password.');
|
||||
|
||||
$authForm = $this->createAuthForm();
|
||||
|
||||
$authForm->username = 'wrong-username';
|
||||
$authForm->password = 'wrong-password';
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
public function testAuthenticateByWrongEmailPass() {
|
||||
$this->expectException(ForbiddenOperationException::class);
|
||||
$this->expectExceptionMessage('Invalid credentials. Invalid email or password.');
|
||||
|
||||
$authForm = $this->createAuthForm();
|
||||
|
||||
$authForm->username = 'wrong-email@ely.by';
|
||||
$authForm->password = 'wrong-password';
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
public function testAuthenticateByValidCredentialsIntoBlockedAccount() {
|
||||
$this->expectException(ForbiddenOperationException::class);
|
||||
$this->expectExceptionMessage('This account has been suspended.');
|
||||
|
||||
$authForm = $this->createAuthForm(Account::STATUS_BANNED);
|
||||
|
||||
$authForm->username = 'dummy';
|
||||
$authForm->password = 'password_0';
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
public function testAuthenticateByValidCredentials() {
|
||||
$authForm = $this->createAuthForm();
|
||||
|
||||
$minecraftAccessKey = new MinecraftAccessKey();
|
||||
$minecraftAccessKey->access_token = Uuid::uuid4();
|
||||
$authForm->expects($this->once())
|
||||
->method('createMinecraftAccessToken')
|
||||
->willReturn($minecraftAccessKey);
|
||||
|
||||
$authForm->username = 'dummy';
|
||||
$authForm = new AuthenticationForm();
|
||||
$authForm->username = 'admin';
|
||||
$authForm->password = 'password_0';
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
|
||||
$result = $authForm->authenticate();
|
||||
$this->assertInstanceOf(AuthenticateData::class, $result);
|
||||
$this->assertSame($minecraftAccessKey->access_token, $result->getMinecraftAccessKey()->access_token);
|
||||
$authForm->clientToken = Uuid::uuid4()->toString();
|
||||
$result = $authForm->authenticate()->getResponseData();
|
||||
$this->assertRegExp('/^[\w=-]+\.[\w=-]+\.[\w=-]+$/', $result['accessToken']);
|
||||
$this->assertSame($authForm->clientToken, $result['clientToken']);
|
||||
$this->assertSame('df936908-b2e1-544d-96f8-2977ec213022', $result['selectedProfile']['id']);
|
||||
$this->assertSame('Admin', $result['selectedProfile']['name']);
|
||||
$this->assertFalse($result['selectedProfile']['legacy']);
|
||||
$this->assertTrue(OauthSession::find()->andWhere([
|
||||
'account_id' => 1,
|
||||
'client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER,
|
||||
])->exists());
|
||||
}
|
||||
|
||||
public function testCreateMinecraftAccessToken() {
|
||||
/**
|
||||
* @dataProvider getInvalidCredentialsCases
|
||||
*/
|
||||
public function testAuthenticateByWrongNicknamePass(string $expectedExceptionMessage, string $login, string $password) {
|
||||
$this->expectException(ForbiddenOperationException::class);
|
||||
$this->expectExceptionMessage($expectedExceptionMessage);
|
||||
|
||||
$authForm = new AuthenticationForm();
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
/** @var Account $account */
|
||||
$account = $this->tester->grabFixture('accounts', 'admin');
|
||||
/** @var MinecraftAccessKey $result */
|
||||
$result = $this->callProtected($authForm, 'createMinecraftAccessToken', $account);
|
||||
$this->assertInstanceOf(MinecraftAccessKey::class, $result);
|
||||
$this->assertSame($account->id, $result->account_id);
|
||||
$this->assertSame($authForm->clientToken, $result->client_token);
|
||||
$this->assertInstanceOf(MinecraftAccessKey::class, MinecraftAccessKey::findOne($result->access_token));
|
||||
$authForm->username = $login;
|
||||
$authForm->password = $password;
|
||||
$authForm->clientToken = Uuid::uuid4()->toString();
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
public function testCreateMinecraftAccessTokenWithExistsClientId() {
|
||||
$authForm = new AuthenticationForm();
|
||||
$minecraftFixture = $this->tester->grabFixture('minecraftAccessKeys', 'admin-token');
|
||||
$authForm->clientToken = $minecraftFixture['client_token'];
|
||||
/** @var Account $account */
|
||||
$account = $this->tester->grabFixture('accounts', 'admin');
|
||||
/** @var MinecraftAccessKey $result */
|
||||
$result = $this->callProtected($authForm, 'createMinecraftAccessToken', $account);
|
||||
$this->assertInstanceOf(MinecraftAccessKey::class, $result);
|
||||
$this->assertSame($account->id, $result->account_id);
|
||||
$this->assertSame($authForm->clientToken, $result->client_token);
|
||||
$this->assertNull(MinecraftAccessKey::findOne($minecraftFixture['access_token']));
|
||||
$this->assertInstanceOf(MinecraftAccessKey::class, MinecraftAccessKey::findOne($result->access_token));
|
||||
}
|
||||
|
||||
private function createAuthForm($status = Account::STATUS_ACTIVE) {
|
||||
/** @var LoginForm|\PHPUnit\Framework\MockObject\MockObject $loginForm */
|
||||
$loginForm = $this->getMockBuilder(LoginForm::class)
|
||||
->setMethods(['getAccount'])
|
||||
->getMock();
|
||||
|
||||
$account = new Account();
|
||||
$account->username = 'dummy';
|
||||
$account->email = 'dummy@ely.by';
|
||||
$account->status = $status;
|
||||
$account->setPassword('password_0');
|
||||
|
||||
$loginForm
|
||||
->method('getAccount')
|
||||
->willReturn($account);
|
||||
|
||||
/** @var AuthenticationForm|\PHPUnit\Framework\MockObject\MockObject $authForm */
|
||||
$authForm = $this->getMockBuilder(AuthenticationForm::class)
|
||||
->setMethods(['createLoginForm', 'createMinecraftAccessToken'])
|
||||
->getMock();
|
||||
|
||||
$authForm
|
||||
->method('createLoginForm')
|
||||
->willReturn($loginForm);
|
||||
|
||||
return $authForm;
|
||||
public function getInvalidCredentialsCases() {
|
||||
yield ['Invalid credentials. Invalid nickname or password.', 'wrong-username', 'wrong-password'];
|
||||
yield ['Invalid credentials. Invalid email or password.', 'wrong-email@ely.by', 'wrong-password'];
|
||||
yield ['This account has been suspended.', 'Banned', 'password_0'];
|
||||
yield ['Account protected with two factor auth.', 'AccountWithEnabledOtp', 'password_0'];
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -22,10 +22,8 @@ class Yii extends \yii\BaseYii {
|
||||
* @property \GuzzleHttp\Client $guzzle
|
||||
* @property \common\components\EmailsRenderer\Component $emailsRenderer
|
||||
* @property \mito\sentry\Component $sentry
|
||||
* @property \api\components\OAuth2\Component $oauth
|
||||
* @property \common\components\StatsD $statsd
|
||||
* @property \yii\queue\Queue $queue
|
||||
* @property \api\components\Tokens\Component $tokens
|
||||
*/
|
||||
abstract class BaseApplication extends yii\base\Application {
|
||||
}
|
||||
@@ -34,8 +32,11 @@ abstract class BaseApplication extends yii\base\Application {
|
||||
* Class WebApplication
|
||||
* Include only Web application related components here
|
||||
*
|
||||
* @property \api\components\User\Component $user User component.
|
||||
* @property \api\components\ReCaptcha\Component $reCaptcha
|
||||
* @property \api\components\User\Component $user
|
||||
* @property \api\components\ReCaptcha\Component $reCaptcha
|
||||
* @property \api\components\OAuth2\Component $oauth
|
||||
* @property \api\components\Tokens\Component $tokens
|
||||
* @property \api\components\Tokens\TokensFactory $tokensFactory
|
||||
*
|
||||
* @method \api\components\User\Component getUser()
|
||||
*/
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user