mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +05:30
Replace separate minecraft access tokens with JWT
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
Reference in New Issue
Block a user