mirror of
https://github.com/elyby/accounts.git
synced 2024-11-26 08:42:01 +05:30
Replace separate minecraft access tokens with JWT
This commit is contained in:
parent
060a4e960a
commit
a81ef5cac2
@ -4,13 +4,13 @@ declare(strict_types=1);
|
||||
namespace api\components\OAuth2\Entities;
|
||||
|
||||
use api\components\OAuth2\Repositories\PublicScopeRepository;
|
||||
use api\components\Tokens\TokensFactory;
|
||||
use DateTimeImmutable;
|
||||
use League\OAuth2\Server\CryptKeyInterface;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
|
||||
use Yii;
|
||||
|
||||
class AccessTokenEntity implements AccessTokenEntityInterface {
|
||||
use EntityTrait;
|
||||
@ -31,7 +31,7 @@ class AccessTokenEntity implements AccessTokenEntityInterface {
|
||||
return $scope->getIdentifier() !== PublicScopeRepository::OFFLINE_ACCESS;
|
||||
});
|
||||
|
||||
$token = TokensFactory::createForOAuthClient($this);
|
||||
$token = Yii::$app->tokensFactory->createForOAuthClient($this);
|
||||
|
||||
$this->scopes = $scopes;
|
||||
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace api\components\Tokens;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Defuse\Crypto\Crypto;
|
||||
use Exception;
|
||||
use Lcobucci\JWT\Builder;
|
||||
use Lcobucci\JWT\Parser;
|
||||
@ -35,6 +36,11 @@ class Component extends BaseComponent {
|
||||
*/
|
||||
public $privateKeyPass;
|
||||
|
||||
/**
|
||||
* @var string|\Defuse\Crypto\Key
|
||||
*/
|
||||
public $encryptionKey;
|
||||
|
||||
/**
|
||||
* @var AlgorithmsManager|null
|
||||
*/
|
||||
@ -45,6 +51,7 @@ 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 {
|
||||
@ -53,11 +60,11 @@ class Component extends BaseComponent {
|
||||
->issuedAt($now->getTimestamp())
|
||||
->expiresAt($now->addHour()->getTimestamp());
|
||||
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 +92,10 @@ class Component extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public function decryptValue(string $encryptedValue): string {
|
||||
return Crypto::decryptWithPassword($encryptedValue, $this->encryptionKey);
|
||||
}
|
||||
|
||||
private function getAlgorithmManager(): AlgorithmsManager {
|
||||
if ($this->algorithmManager === null) {
|
||||
$this->algorithmManager = new AlgorithmsManager([
|
||||
@ -100,4 +111,12 @@ class Component extends BaseComponent {
|
||||
return $this->algorithmManager;
|
||||
}
|
||||
|
||||
private function prepareValue($value) {
|
||||
if ($value instanceof EncryptedValue) {
|
||||
return Crypto::encryptWithPassword($value->getValue(), $this->encryptionKey);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -3,6 +3,8 @@ 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;
|
||||
@ -10,16 +12,17 @@ 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::buildSub($account->id),
|
||||
'ely-scopes' => R::ACCOUNTS_WEB_USER,
|
||||
'sub' => $this->buildSub($account->id),
|
||||
];
|
||||
if ($session === null) {
|
||||
// If we don't remember a session, the token should live longer
|
||||
@ -32,26 +35,39 @@ class TokensFactory {
|
||||
return Yii::$app->tokens->create($payloads);
|
||||
}
|
||||
|
||||
public static function createForOAuthClient(AccessTokenEntityInterface $accessToken): Token {
|
||||
public function createForOAuthClient(AccessTokenEntityInterface $accessToken): Token {
|
||||
$payloads = [
|
||||
'aud' => self::buildAud($accessToken->getClient()->getIdentifier()),
|
||||
'ely-scopes' => implode(',', array_map(static function(ScopeEntityInterface $scope): string {
|
||||
'aud' => $this->buildAud($accessToken->getClient()->getIdentifier()),
|
||||
'ely-scopes' => $this->joinScopes(array_map(static function(ScopeEntityInterface $scope): string {
|
||||
return $scope->getIdentifier();
|
||||
}, $accessToken->getScopes())),
|
||||
'exp' => $accessToken->getExpiryDateTime()->getTimestamp(),
|
||||
];
|
||||
if ($accessToken->getUserIdentifier() !== null) {
|
||||
$payloads['sub'] = self::buildSub($accessToken->getUserIdentifier());
|
||||
$payloads['sub'] = $this->buildSub($accessToken->getUserIdentifier());
|
||||
}
|
||||
|
||||
return Yii::$app->tokens->create($payloads);
|
||||
}
|
||||
|
||||
private static function buildSub(int $accountId): string {
|
||||
public function createForMinecraftAccount(Account $account, string $clientToken): Token {
|
||||
return Yii::$app->tokens->create([
|
||||
'ely-scopes' => $this->joinScopes([P::MINECRAFT_SERVER_SESSION]),
|
||||
'ely-client-token' => new EncryptedValue($clientToken),
|
||||
'sub' => $this->buildSub($account->id),
|
||||
'exp' => Carbon::now()->addDays(2)->getTimestamp(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function joinScopes(array $scopes): string {
|
||||
return implode(',', $scopes);
|
||||
}
|
||||
|
||||
private function buildSub(int $accountId): string {
|
||||
return self::SUB_ACCOUNT_PREFIX . $accountId;
|
||||
}
|
||||
|
||||
private static function buildAud(string $clientId): string {
|
||||
private function buildAud(string $clientId): string {
|
||||
return self::AUD_CLIENT_PREFIX . $clientId;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ return [
|
||||
'privateKeyPath' => codecept_data_dir('certs/private.pem'),
|
||||
'privateKeyPass' => null,
|
||||
'publicKeyPath' => codecept_data_dir('certs/public.pem'),
|
||||
'encryptionKey' => 'mock-encryption-key',
|
||||
],
|
||||
'reCaptcha' => [
|
||||
'public' => 'public-key',
|
||||
|
@ -21,6 +21,10 @@ return [
|
||||
'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,
|
||||
|
@ -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;
|
||||
@ -9,17 +11,26 @@ use api\modules\authserver\validators\ClientTokenValidator;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
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 +39,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 +81,14 @@ 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);
|
||||
|
||||
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,71 @@
|
||||
<?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\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
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();
|
||||
|
||||
/** @var MinecraftAccessKey|null $accessToken */
|
||||
$accessToken = MinecraftAccessKey::findOne([
|
||||
'access_token' => $this->accessToken,
|
||||
'client_token' => $this->clientToken,
|
||||
]);
|
||||
if ($accessToken === null) {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
if (mb_strlen($this->accessToken) === 36) {
|
||||
/** @var MinecraftAccessKey $token */
|
||||
$token = MinecraftAccessKey::findOne([
|
||||
'access_token' => $this->accessToken,
|
||||
'client_token' => $this->clientToken,
|
||||
]);
|
||||
$account = $token->account;
|
||||
} else {
|
||||
$token = Yii::$app->tokens->parse($this->accessToken);
|
||||
|
||||
$encodedClientToken = $token->getClaim('ely-client-token');
|
||||
$clientToken = Yii::$app->tokens->decryptValue($encodedClientToken);
|
||||
if ($clientToken !== $this->clientToken) {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
}
|
||||
|
||||
$accountClaim = $token->getClaim('sub');
|
||||
$accountId = (int)explode('|', $accountClaim)[1];
|
||||
$account = Account::findOne(['id' => $accountId]);
|
||||
}
|
||||
|
||||
if ($accessToken->account->status === Account::STATUS_BANNED) {
|
||||
if ($account === null || $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);
|
||||
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,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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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,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,49 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @example {"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE1NzU0Nzk1NTMsImV4cCI6MTU3NTY1MjM1MywiZWx5LXNjb3BlcyI6Im1pbmVjcmFmdF9zZXJ2ZXJfc2Vzc2lvbiIsImVseS1jbGllbnQtdG9rZW4iOiJkZWY1MDIwMDE2ZTEzMTBmMzM2YzVjYWQzZDdiMTJmYjcyNmVhYzdlYjgyOGUzMzg1MzBhMmFmODdkZTJhMjRiMTVmNzAxNWQ1MjU1MjhiNGZiMjgzMTgxOTA2ODhlMWE4Njk5MjAwMzBlMTQyZmQ5ZWM5ODBlZDkzMWI1Mzc2MzgyMTliMjVjMjI1MjQyYzdmMjgzMjE0NjcyNDg3ZDQ4MTYxYjMwMGU1MGIzYWJlMTYwYjVkMmE4ZWMyMzMwMGJhMGNlMTg3MzYyYTgyMjJiYjQ4OTU0MzM4MDJiNTBlZDBhYzFhMWUwZDk3NDgxNDciLCJzdWIiOiJlbHl8MSJ9.PuM-8rzj4qtD9l0lUANSIWC8yjJe8ifarOYsAjc3r4iYFt0P6za-gzJEPncDC80oCXsYVlJHtrEypcsB9wJFSg", "clientToken": "d1b1162c-3d73-4b35-b64f-7bf68bd0e853"}
|
||||
* @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 +56,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 +70,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 +81,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();
|
||||
|
@ -14,7 +14,9 @@ class TokensFactoryTest extends TestCase {
|
||||
$account = new Account();
|
||||
$account->id = 1;
|
||||
|
||||
$token = TokensFactory::createForAccount($account);
|
||||
$factory = new TokensFactory();
|
||||
|
||||
$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 +26,7 @@ class TokensFactoryTest extends TestCase {
|
||||
$session = new AccountSession();
|
||||
$session->id = 2;
|
||||
|
||||
$token = TokensFactory::createForAccount($account, $session);
|
||||
$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'));
|
||||
|
@ -79,7 +79,7 @@ class AuthenticationFormTest extends TestCase {
|
||||
|
||||
$result = $authForm->authenticate();
|
||||
$this->assertInstanceOf(AuthenticateData::class, $result);
|
||||
$this->assertSame($minecraftAccessKey->access_token, $result->getMinecraftAccessKey()->access_token);
|
||||
$this->assertSame($minecraftAccessKey->access_token, $result->getToken()->access_token);
|
||||
}
|
||||
|
||||
public function testCreateMinecraftAccessToken() {
|
||||
|
@ -24,7 +24,6 @@ class Yii extends \yii\BaseYii {
|
||||
* @property \mito\sentry\Component $sentry
|
||||
* @property \common\components\StatsD $statsd
|
||||
* @property \yii\queue\Queue $queue
|
||||
* @property \api\components\Tokens\Component $tokens
|
||||
*/
|
||||
abstract class BaseApplication extends yii\base\Application {
|
||||
}
|
||||
@ -33,9 +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\OAuth2\Component $oauth
|
||||
* @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()
|
||||
*/
|
||||
|
@ -13,6 +13,7 @@
|
||||
"ext-pdo": "*",
|
||||
"ext-simplexml": "*",
|
||||
"bacon/bacon-qr-code": "^1.0",
|
||||
"defuse/php-encryption": "^2.2",
|
||||
"domnikl/statsd": "^2.6",
|
||||
"ely/mojang-api": "^0.2.0",
|
||||
"ely/yii2-tempmail-validator": "^2.0",
|
||||
|
2
composer.lock
generated
2
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "7ee1d380684b79ffabf92f115d7de4a8",
|
||||
"content-hash": "10af6b999939a9f213664883387184ed",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
|
Loading…
Reference in New Issue
Block a user