mirror of
https://github.com/elyby/accounts.git
synced 2025-02-04 07:59:35 +05:30
Merge branch 'authserver_intergration'
This commit is contained in:
commit
762fab447b
19
api/components/ErrorHandler.php
Normal file
19
api/components/ErrorHandler.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace api\components;
|
||||
|
||||
use api\modules\authserver\exceptions\AuthserverException;
|
||||
|
||||
class ErrorHandler extends \yii\web\ErrorHandler {
|
||||
|
||||
public function convertExceptionToArray($exception) {
|
||||
if ($exception instanceof AuthserverException) {
|
||||
return [
|
||||
'error' => $exception->getName(),
|
||||
'errorMessage' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
return parent::convertExceptionToArray($exception);
|
||||
}
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ $params = array_merge(
|
||||
return [
|
||||
'id' => 'accounts-site-api',
|
||||
'basePath' => dirname(__DIR__),
|
||||
'bootstrap' => ['log'],
|
||||
'bootstrap' => ['log', 'authserver'],
|
||||
'controllerNamespace' => 'api\controllers',
|
||||
'params' => $params,
|
||||
'components' => [
|
||||
@ -47,5 +47,14 @@ return [
|
||||
'class' => \common\components\oauth\Component::class,
|
||||
'grantTypes' => ['authorization_code'],
|
||||
],
|
||||
'errorHandler' => [
|
||||
'class' => \api\components\ErrorHandler::class,
|
||||
],
|
||||
],
|
||||
'modules' => [
|
||||
'authserver' => [
|
||||
'class' => \api\modules\authserver\Module::class,
|
||||
'baseDomain' => $params['authserverDomain'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -17,13 +17,10 @@ class ActiveUserRule extends AccessRule {
|
||||
protected function matchCustom($action) {
|
||||
$account = $this->getIdentity();
|
||||
|
||||
return $account->status > Account::STATUS_REGISTERED
|
||||
return $account->status === Account::STATUS_ACTIVE
|
||||
&& $account->isAgreedWithActualRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \api\models\AccountIdentity|null
|
||||
*/
|
||||
protected function getIdentity() {
|
||||
return Yii::$app->getUser()->getIdentity();
|
||||
}
|
||||
|
@ -54,7 +54,11 @@ class LoginForm extends ApiForm {
|
||||
public function validateActivity($attribute) {
|
||||
if (!$this->hasErrors()) {
|
||||
$account = $this->getAccount();
|
||||
if ($account->status !== Account::STATUS_ACTIVE) {
|
||||
if ($account->status === Account::STATUS_BANNED) {
|
||||
$this->addError($attribute, E::ACCOUNT_BANNED);
|
||||
}
|
||||
|
||||
if ($account->status === Account::STATUS_REGISTERED) {
|
||||
$this->addError($attribute, E::ACCOUNT_NOT_ACTIVATED);
|
||||
}
|
||||
}
|
||||
|
43
api/modules/authserver/Module.php
Normal file
43
api/modules/authserver/Module.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace api\modules\authserver;
|
||||
|
||||
use Yii;
|
||||
use yii\base\BootstrapInterface;
|
||||
use yii\base\InvalidConfigException;
|
||||
|
||||
class Module extends \yii\base\Module implements BootstrapInterface {
|
||||
|
||||
public $id = 'authserver';
|
||||
|
||||
public $defaultRoute = 'index';
|
||||
|
||||
/**
|
||||
* @var string базовый домен, запросы на который этот модуль должен обрабатывать
|
||||
*/
|
||||
public $baseDomain = 'https://authserver.ely.by';
|
||||
|
||||
public function init() {
|
||||
parent::init();
|
||||
if ($this->baseDomain === null) {
|
||||
throw new InvalidConfigException('base domain must be specified');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\base\Application $app the application currently running
|
||||
*/
|
||||
public function bootstrap($app) {
|
||||
$app->getUrlManager()->addRules([
|
||||
$this->baseDomain . '/' . $this->id . '/auth/<action>' => $this->id . '/authentication/<action>',
|
||||
], false);
|
||||
}
|
||||
|
||||
public static function info($message) {
|
||||
Yii::info($message, 'legacy-authserver');
|
||||
}
|
||||
|
||||
public static function error($message) {
|
||||
Yii::info($message, 'legacy-authserver');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\controllers;
|
||||
|
||||
use api\controllers\Controller;
|
||||
use api\modules\authserver\models;
|
||||
|
||||
class AuthenticationController extends Controller {
|
||||
|
||||
public function behaviors() {
|
||||
$behaviors = parent::behaviors();
|
||||
unset($behaviors['authenticator']);
|
||||
|
||||
return $behaviors;
|
||||
}
|
||||
|
||||
public function verbs() {
|
||||
return [
|
||||
'authenticate' => ['POST'],
|
||||
'refresh' => ['POST'],
|
||||
'validate' => ['POST'],
|
||||
'signout' => ['POST'],
|
||||
'invalidate' => ['POST'],
|
||||
];
|
||||
}
|
||||
|
||||
public function actionAuthenticate() {
|
||||
$model = new models\AuthenticationForm();
|
||||
$model->loadByPost();
|
||||
|
||||
return $model->authenticate()->getResponseData(true);
|
||||
}
|
||||
|
||||
public function actionRefresh() {
|
||||
$model = new models\RefreshTokenForm();
|
||||
$model->loadByPost();
|
||||
|
||||
return $model->refresh()->getResponseData(false);
|
||||
}
|
||||
|
||||
public function actionValidate() {
|
||||
$model = new models\ValidateForm();
|
||||
$model->loadByPost();
|
||||
$model->validateToken();
|
||||
// В случае успеха ожидается пустой ответ. В случае ошибки же бросается исключение,
|
||||
// которое обработает ErrorHandler
|
||||
}
|
||||
|
||||
public function actionSignout() {
|
||||
$model = new models\SignoutForm();
|
||||
$model->loadByPost();
|
||||
$model->signout();
|
||||
// В случае успеха ожидается пустой ответ. В случае ошибки же бросается исключение,
|
||||
// которое обработает ErrorHandler
|
||||
}
|
||||
|
||||
public function actionInvalidate() {
|
||||
$model = new models\InvalidateForm();
|
||||
$model->loadByPost();
|
||||
$model->invalidateToken();
|
||||
// В случае успеха ожидается пустой ответ. В случае ошибки же бросается исключение,
|
||||
// которое обработает ErrorHandler
|
||||
}
|
||||
|
||||
}
|
15
api/modules/authserver/controllers/IndexController.php
Normal file
15
api/modules/authserver/controllers/IndexController.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\controllers;
|
||||
|
||||
use api\controllers\Controller;
|
||||
|
||||
class IndexController extends Controller {
|
||||
|
||||
// TODO: симулировать для этого модуля обработчик 404 ошибок, как был в фалконе
|
||||
public function notFoundAction() {
|
||||
/*return $this->response
|
||||
->setStatusCode(404, 'Not Found')
|
||||
->setContent('Page not found. Check our <a href="http://docs.ely.by">documentation site</a>.');*/
|
||||
}
|
||||
|
||||
}
|
19
api/modules/authserver/exceptions/AuthserverException.php
Normal file
19
api/modules/authserver/exceptions/AuthserverException.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\exceptions;
|
||||
|
||||
use ReflectionClass;
|
||||
use yii\web\HttpException;
|
||||
|
||||
class AuthserverException extends HttpException {
|
||||
|
||||
/**
|
||||
* Рефлексия быстрее, как ни странно:
|
||||
* @url https://coderwall.com/p/cpxxxw/php-get-class-name-without-namespace#comment_19313
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
return (new ReflectionClass($this))->getShortName();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\exceptions;
|
||||
|
||||
class ForbiddenOperationException extends AuthserverException {
|
||||
|
||||
public function __construct($message, $code = 0, \Exception $previous = null) {
|
||||
parent::__construct($status = 401, $message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\exceptions;
|
||||
|
||||
class IllegalArgumentException extends AuthserverException {
|
||||
|
||||
public function __construct($status = null, $message = null, $code = 0, \Exception $previous = null) {
|
||||
parent::__construct(400, 'credentials can not be null.', $code, $previous);
|
||||
}
|
||||
|
||||
}
|
44
api/modules/authserver/models/AuthenticateData.php
Normal file
44
api/modules/authserver/models/AuthenticateData.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use common\models\MinecraftAccessKey;
|
||||
|
||||
class AuthenticateData {
|
||||
|
||||
/**
|
||||
* @var MinecraftAccessKey
|
||||
*/
|
||||
private $minecraftAccessKey;
|
||||
|
||||
public function __construct(MinecraftAccessKey $minecraftAccessKey) {
|
||||
$this->minecraftAccessKey = $minecraftAccessKey;
|
||||
}
|
||||
|
||||
public function getMinecraftAccessKey() : MinecraftAccessKey {
|
||||
return $this->minecraftAccessKey;
|
||||
}
|
||||
|
||||
public function getResponseData(bool $includeAvailableProfiles = false) : array {
|
||||
$accessKey = $this->minecraftAccessKey;
|
||||
$account = $accessKey->account;
|
||||
|
||||
$result = [
|
||||
'accessToken' => $accessKey->access_token,
|
||||
'clientToken' => $accessKey->client_token,
|
||||
'selectedProfile' => [
|
||||
'id' => $account->uuid,
|
||||
'name' => $account->username,
|
||||
'legacy' => false,
|
||||
],
|
||||
];
|
||||
|
||||
if ($includeAvailableProfiles) {
|
||||
// Сами моянги ещё ничего не придумали с этими availableProfiles
|
||||
$availableProfiles[0] = $result['selectedProfile'];
|
||||
$result['availableProfiles'] = $availableProfiles;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
92
api/modules/authserver/models/AuthenticationForm.php
Normal file
92
api/modules/authserver/models/AuthenticationForm.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\models\authentication\LoginForm;
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\Module as Authserver;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
|
||||
class AuthenticationForm extends Form {
|
||||
|
||||
public $username;
|
||||
public $password;
|
||||
public $clientToken;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
[['username', 'password', 'clientToken'], RequiredValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthenticateData
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
*/
|
||||
public function authenticate() {
|
||||
$this->validate();
|
||||
|
||||
Authserver::info("Trying to authenticate user by login = '{$this->username}'.");
|
||||
|
||||
$loginForm = $this->createLoginForm();
|
||||
$loginForm->login = $this->username;
|
||||
$loginForm->password = $this->password;
|
||||
if (!$loginForm->validate()) {
|
||||
$errors = $loginForm->getFirstErrors();
|
||||
if (isset($errors['login'])) {
|
||||
if ($errors['login'] === E::ACCOUNT_BANNED) {
|
||||
Authserver::error("User with login = '{$this->username}' is banned");
|
||||
throw new ForbiddenOperationException('This account has been suspended.');
|
||||
} else {
|
||||
Authserver::error("Cannot find user by login = '{$this->username}'");
|
||||
}
|
||||
} elseif (isset($errors['password'])) {
|
||||
Authserver::error("User with login = '{$this->username}' passed wrong password.");
|
||||
}
|
||||
|
||||
// На старом сервере авторизации использовалось поле nickname, а не username, так что сохраняем эту логику
|
||||
$attribute = $loginForm->getLoginAttribute();
|
||||
if ($attribute === 'username') {
|
||||
$attribute = 'nickname';
|
||||
}
|
||||
|
||||
// TODO: эта логика дублируется с логикой в SignoutForm
|
||||
|
||||
throw new ForbiddenOperationException("Invalid credentials. Invalid {$attribute} or password.");
|
||||
}
|
||||
|
||||
$account = $loginForm->getAccount();
|
||||
$accessTokenModel = $this->createMinecraftAccessToken($account);
|
||||
$dataModel = new AuthenticateData($accessTokenModel);
|
||||
|
||||
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([
|
||||
'client_token' => $this->clientToken,
|
||||
'account_id' => $account->id,
|
||||
]);
|
||||
|
||||
if ($accessTokenModel === null) {
|
||||
$accessTokenModel = new MinecraftAccessKey();
|
||||
$accessTokenModel->client_token = $this->clientToken;
|
||||
$accessTokenModel->account_id = $account->id;
|
||||
$accessTokenModel->insert();
|
||||
} else {
|
||||
$accessTokenModel->refreshPrimaryKeyValue();
|
||||
}
|
||||
|
||||
return $accessTokenModel;
|
||||
}
|
||||
|
||||
protected function createLoginForm() : LoginForm {
|
||||
return new LoginForm();
|
||||
}
|
||||
|
||||
}
|
27
api/modules/authserver/models/Form.php
Normal file
27
api/modules/authserver/models/Form.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use Yii;
|
||||
use yii\base\Model;
|
||||
|
||||
abstract class Form extends Model {
|
||||
|
||||
public function formName() {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function loadByGet() {
|
||||
return $this->load(Yii::$app->request->get());
|
||||
}
|
||||
|
||||
public function loadByPost() {
|
||||
$data = Yii::$app->request->post();
|
||||
// TODO: проверить, парсит ли Yii2 raw body и что он делает, если там неспаршенный json
|
||||
/*if (empty($data)) {
|
||||
$data = $request->getJsonRawBody(true);
|
||||
}*/
|
||||
|
||||
return $this->load($data);
|
||||
}
|
||||
|
||||
}
|
37
api/modules/authserver/models/InvalidateForm.php
Normal file
37
api/modules/authserver/models/InvalidateForm.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\models\MinecraftAccessKey;
|
||||
|
||||
class InvalidateForm extends Form {
|
||||
|
||||
public $accessToken;
|
||||
public $clientToken;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
[['accessToken', 'clientToken'], RequiredValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
*/
|
||||
public function invalidateToken() : bool {
|
||||
$this->validate();
|
||||
|
||||
$token = MinecraftAccessKey::findOne([
|
||||
'access_token' => $this->accessToken,
|
||||
'client_token' => $this->clientToken,
|
||||
]);
|
||||
|
||||
if ($token !== null) {
|
||||
$token->delete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
48
api/modules/authserver/models/RefreshTokenForm.php
Normal file
48
api/modules/authserver/models/RefreshTokenForm.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
|
||||
class RefreshTokenForm extends Form {
|
||||
|
||||
public $accessToken;
|
||||
public $clientToken;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
[['accessToken', 'clientToken'], RequiredValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthenticateData
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
*/
|
||||
public function refresh() {
|
||||
$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 ($accessToken->account->status === Account::STATUS_BANNED) {
|
||||
throw new ForbiddenOperationException('This account has been suspended.');
|
||||
}
|
||||
|
||||
$accessToken->refreshPrimaryKeyValue();
|
||||
$accessToken->update();
|
||||
|
||||
$dataModel = new AuthenticateData($accessToken);
|
||||
|
||||
return $dataModel;
|
||||
}
|
||||
|
||||
}
|
58
api/modules/authserver/models/SignoutForm.php
Normal file
58
api/modules/authserver/models/SignoutForm.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\models\authentication\LoginForm;
|
||||
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 Form {
|
||||
|
||||
public $username;
|
||||
public $password;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
[['username', 'password'], RequiredValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
public function signout() : bool {
|
||||
$this->validate();
|
||||
|
||||
$loginForm = new LoginForm();
|
||||
$loginForm->login = $this->username;
|
||||
$loginForm->password = $this->password;
|
||||
if (!$loginForm->validate()) {
|
||||
$errors = $loginForm->getFirstErrors();
|
||||
if (isset($errors['login']) && $errors['login'] === E::ACCOUNT_BANNED) {
|
||||
// Считаем, что заблокированный может безболезненно выйти
|
||||
return true;
|
||||
}
|
||||
|
||||
// На старом сервере авторизации использовалось поле nickname, а не username, так что сохраняем эту логику
|
||||
$attribute = $loginForm->getLoginAttribute();
|
||||
if ($attribute === 'username') {
|
||||
$attribute = 'nickname';
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
35
api/modules/authserver/models/ValidateForm.php
Normal file
35
api/modules/authserver/models/ValidateForm.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\models;
|
||||
|
||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use common\models\MinecraftAccessKey;
|
||||
|
||||
class ValidateForm extends Form {
|
||||
|
||||
public $accessToken;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
[['accessToken'], RequiredValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
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->isActual()) {
|
||||
$result->delete();
|
||||
throw new ForbiddenOperationException('Token expired.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
25
api/modules/authserver/validators/RequiredValidator.php
Normal file
25
api/modules/authserver/validators/RequiredValidator.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace api\modules\authserver\validators;
|
||||
|
||||
use api\modules\authserver\exceptions\IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Для данного модуля нам не принципиально, что там за ошибка: если не хватает хотя бы одного
|
||||
* параметра - тут же отправляем исключение и дело с концом
|
||||
*/
|
||||
class RequiredValidator extends \yii\validators\RequiredValidator {
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return null
|
||||
* @throws \api\modules\authserver\exceptions\AuthserverException
|
||||
*/
|
||||
protected function validateValue($value) {
|
||||
if (parent::validateValue($value) !== null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -25,18 +25,21 @@ class PrimaryKeyValueBehavior extends Behavior {
|
||||
}
|
||||
|
||||
public function setPrimaryKeyValue() : bool {
|
||||
$owner = $this->owner;
|
||||
if ($owner->getPrimaryKey() === null) {
|
||||
do {
|
||||
$key = $this->generateValue();
|
||||
} while ($this->isValueExists($key));
|
||||
|
||||
$owner->{$this->getPrimaryKeyName()} = $key;
|
||||
if ($this->owner->getPrimaryKey() === null) {
|
||||
$this->refreshPrimaryKeyValue();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function refreshPrimaryKeyValue() {
|
||||
do {
|
||||
$key = $this->generateValue();
|
||||
} while ($this->isValueExists($key));
|
||||
|
||||
$this->owner->{$this->getPrimaryKeyName()} = $key;
|
||||
}
|
||||
|
||||
protected function generateValue() : string {
|
||||
return (string)call_user_func($this->value);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class UuidAlgorithm extends DefaultAlgorithm implements KeyAlgorithmInterface {
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function generate($len = 40) : string {
|
||||
return Uuid::uuid5(Uuid::NAMESPACE_DNS, parent::generate($len))->toString();
|
||||
return Uuid::uuid4()->toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ final class Error {
|
||||
const KEY_NOT_EXISTS = 'error.key_not_exists';
|
||||
const KEY_EXPIRE = 'error.key_expire';
|
||||
|
||||
const ACCOUNT_BANNED = 'error.account_banned';
|
||||
const ACCOUNT_NOT_ACTIVATED = 'error.account_not_activated';
|
||||
const ACCOUNT_ALREADY_ACTIVATED = 'error.account_already_activated';
|
||||
const ACCOUNT_CANNOT_RESEND_MESSAGE = 'error.account_cannot_resend_message';
|
||||
|
@ -43,6 +43,7 @@ use const common\LATEST_RULES_VERSION;
|
||||
class Account extends ActiveRecord {
|
||||
|
||||
const STATUS_DELETED = -10;
|
||||
const STATUS_BANNED = -1;
|
||||
const STATUS_REGISTERED = 0;
|
||||
const STATUS_ACTIVE = 10;
|
||||
|
||||
|
60
common/models/MinecraftAccessKey.php
Normal file
60
common/models/MinecraftAccessKey.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace common\models;
|
||||
|
||||
use common\behaviors\PrimaryKeyValueBehavior;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use yii\behaviors\TimestampBehavior;
|
||||
use yii\db\ActiveQuery;
|
||||
use yii\db\ActiveRecord;
|
||||
|
||||
/**
|
||||
* Это временный класс, куда мигрирует вся логика ныне существующего authserver.ely.by.
|
||||
* Поскольку там допускался вход по логину и паролю, а формат хранения выданных токенов был
|
||||
* иным, то на период, пока мы окончательно не мигрируем, нужно сохранить старую логику
|
||||
* и структуру под неё.
|
||||
*
|
||||
* Поля модели:
|
||||
* @property string $access_token
|
||||
* @property string $client_token
|
||||
* @property integer $account_id
|
||||
* @property integer $created_at
|
||||
* @property integer $updated_at
|
||||
*
|
||||
* Отношения:
|
||||
* @property Account $account
|
||||
*
|
||||
* Поведения:
|
||||
* @mixin TimestampBehavior
|
||||
* @mixin PrimaryKeyValueBehavior
|
||||
*/
|
||||
class MinecraftAccessKey extends ActiveRecord {
|
||||
|
||||
const LIFETIME = 172800; // Ключ актуален в течение 2 дней
|
||||
|
||||
public static function tableName() {
|
||||
return '{{%minecraft_access_keys}}';
|
||||
}
|
||||
|
||||
public function behaviors() {
|
||||
return [
|
||||
[
|
||||
'class' => TimestampBehavior::class,
|
||||
],
|
||||
[
|
||||
'class' => PrimaryKeyValueBehavior::class,
|
||||
'value' => function() {
|
||||
return Uuid::uuid4()->toString();
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount() : ActiveQuery {
|
||||
return $this->hasOne(Account::class, ['id' => 'account_id']);
|
||||
}
|
||||
|
||||
public function isActual() : bool {
|
||||
return $this->updated_at + self::LIFETIME >= time();
|
||||
}
|
||||
|
||||
}
|
@ -14,14 +14,14 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"require": {
|
||||
"php": "~7.0.6",
|
||||
"php": "^7.0.6",
|
||||
"yiisoft/yii2": "2.0.9",
|
||||
"yiisoft/yii2-swiftmailer": "*",
|
||||
"ramsey/uuid": "~3.1",
|
||||
"ramsey/uuid": "^3.5.0",
|
||||
"league/oauth2-server": "~4.1.5",
|
||||
"yiisoft/yii2-redis": "~2.0.0",
|
||||
"guzzlehttp/guzzle": "^6.0.0",
|
||||
"php-amqplib/php-amqplib": "~2.6.2",
|
||||
"php-amqplib/php-amqplib": "^2.6.2",
|
||||
"ely/yii2-tempmail-validator": "~1.0.0",
|
||||
"emarref/jwt": "~1.0.2",
|
||||
"ely/amqp-controller": "^0.1.0"
|
||||
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use console\db\Migration;
|
||||
|
||||
class m160819_211139_minecraft_access_tokens extends Migration {
|
||||
|
||||
public function safeUp() {
|
||||
$this->createTable('{{%minecraft_access_keys}}', [
|
||||
'access_token' => $this->string(36)->notNull(),
|
||||
'client_token' => $this->string(36)->notNull(),
|
||||
'account_id' => $this->db->getTableSchema('{{%accounts}}')->getColumn('id')->dbType . ' NOT NULL',
|
||||
'created_at' => $this->integer()->unsigned()->notNull(),
|
||||
'updated_at' => $this->integer()->unsigned()->notNull(),
|
||||
]);
|
||||
|
||||
$this->addPrimaryKey('access_token', '{{%minecraft_access_keys}}', 'access_token');
|
||||
$this->addForeignKey('FK_minecraft_access_token_to_account', '{{%minecraft_access_keys}}', 'account_id', '{{%accounts}}', 'id', 'CASCADE', 'CASCADE');
|
||||
$this->createIndex('client_token', '{{%minecraft_access_keys}}', 'client_token', true);
|
||||
}
|
||||
|
||||
public function safeDown() {
|
||||
$this->dropTable('{{%minecraft_access_keys}}');
|
||||
}
|
||||
|
||||
}
|
@ -14,12 +14,13 @@ services:
|
||||
|
||||
web:
|
||||
build: ./docker/nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
links:
|
||||
- app
|
||||
volumes_from:
|
||||
- app
|
||||
environment:
|
||||
- AUTHSERVER_HOST=authserver.ely.by
|
||||
- PHP_LINK=app
|
||||
|
||||
node-dev-server:
|
||||
build: ./frontend
|
||||
|
@ -1,3 +1,10 @@
|
||||
FROM nginx:1.9
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY account.ely.by.conf.template /etc/nginx/conf.d/account.ely.by.conf.template
|
||||
COPY run.sh /run.sh
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf \
|
||||
&& chmod a+x /run.sh
|
||||
|
||||
CMD ["/run.sh"]
|
||||
|
54
docker/nginx/account.ely.by.conf.template
Normal file
54
docker/nginx/account.ely.by.conf.template
Normal file
@ -0,0 +1,54 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
set $root_path '/var/www/html';
|
||||
set $api_path '${root_path}/api/web';
|
||||
set $frontend_path '${root_path}/frontend/dist';
|
||||
set $authserver_host '${AUTHSERVER_HOST}';
|
||||
|
||||
root $root_path;
|
||||
charset utf-8;
|
||||
client_max_body_size 2M;
|
||||
etag on;
|
||||
|
||||
set $request_url $request_uri;
|
||||
if ($host = $authserver_host) {
|
||||
set $request_url '/api/authserver${request_uri}';
|
||||
}
|
||||
|
||||
location / {
|
||||
if ($host = $authserver_host) {
|
||||
rewrite ^ /api/authserver$uri last;
|
||||
}
|
||||
|
||||
alias $frontend_path;
|
||||
index index.html;
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
location /api {
|
||||
try_files $uri $uri /api/web/index.php?$is_args$args;
|
||||
}
|
||||
|
||||
location ~* \.php$ {
|
||||
fastcgi_pass ${PHP_LINK}:9000;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SERVER_NAME $host;
|
||||
fastcgi_param REQUEST_URI $request_url;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# html файлы идут отдельно, для них будет применяться E-Tag кэширование
|
||||
location ~* \.html$ {
|
||||
root $frontend_path;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Раздача статики для frontend с указанием max-кэша. Сброс будет по #hash после ребилда webpackом
|
||||
location ~* ^.+\.(jpg|jpeg|gif|png|svg|js|json|css|zip|rar|eot|ttf|woff|ico) {
|
||||
root $frontend_path;
|
||||
expires max;
|
||||
access_log off;
|
||||
}
|
||||
}
|
@ -21,47 +21,5 @@ http {
|
||||
sendfile on;
|
||||
keepalive_timeout 10;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
set $root_path '/var/www/html';
|
||||
set $api_path '${root_path}/api/web';
|
||||
set $frontend_path '${root_path}/frontend/dist';
|
||||
|
||||
root $root_path;
|
||||
charset utf-8;
|
||||
client_max_body_size 2M;
|
||||
etag on;
|
||||
|
||||
location / {
|
||||
alias $frontend_path;
|
||||
index index.html;
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
location /api {
|
||||
try_files $uri /api/web/index.php?$args;
|
||||
}
|
||||
|
||||
location ~* \.php$ {
|
||||
fastcgi_pass app:9000;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SERVER_NAME $host;
|
||||
}
|
||||
|
||||
# html файлы идут отдельно, для них будет применяться E-Tag кэширование
|
||||
location ~* \.html$ {
|
||||
root $frontend_path;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Раздача статики для frontend с указанием max-кэша. Сброс будет по #hash после ребилда webpackом
|
||||
location ~* ^.+\.(jpg|jpeg|gif|png|svg|js|json|css|zip|rar|eot|ttf|woff|ico) {
|
||||
root $frontend_path;
|
||||
expires max;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
4
docker/nginx/run.sh
Normal file
4
docker/nginx/run.sh
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
envsubst '$AUTHSERVER_HOST:$PHP_LINK' < /etc/nginx/conf.d/account.ely.by.conf.template > /etc/nginx/conf.d/default.conf
|
||||
nginx -g 'daemon off;'
|
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
return [
|
||||
'userSecret' => 'some-long-secret-key',
|
||||
'authserverDomain' => 'http://authserver.ely.by.local',
|
||||
];
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
return [
|
||||
'userSecret' => 'some-long-secret-key',
|
||||
'authserverDomain' => 'https://authserver.ely.by',
|
||||
];
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
return [
|
||||
'userSecret' => 'some-long-secret-key',
|
||||
'authserverDomain' => 'https://authserver.ely.by',
|
||||
];
|
||||
|
36
tests/codeception/api/_pages/AuthserverRoute.php
Normal file
36
tests/codeception/api/_pages/AuthserverRoute.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\_pages;
|
||||
|
||||
use yii\codeception\BasePage;
|
||||
|
||||
/**
|
||||
* @property \tests\codeception\api\FunctionalTester $actor
|
||||
*/
|
||||
class AuthserverRoute extends BasePage {
|
||||
|
||||
public function authenticate($params) {
|
||||
$this->route = ['authserver/authentication/authenticate'];
|
||||
$this->actor->sendPOST($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function refresh($params) {
|
||||
$this->route = ['authserver/authentication/refresh'];
|
||||
$this->actor->sendPOST($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function validate($params) {
|
||||
$this->route = ['authserver/authentication/validate'];
|
||||
$this->actor->sendPOST($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function invalidate($params) {
|
||||
$this->route = ['authserver/authentication/invalidate'];
|
||||
$this->actor->sendPOST($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function signout($params) {
|
||||
$this->route = ['authserver/authentication/signout'];
|
||||
$this->actor->sendPOST($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace tests\codeception\api\_pages;
|
||||
|
||||
use yii\codeception\BasePage;
|
||||
|
||||
/**
|
||||
* Represents contact page
|
||||
* @property \tests\codeception\api\FunctionalTester $actor
|
||||
*/
|
||||
class ContactPage extends BasePage {
|
||||
|
||||
public $route = 'site/contact';
|
||||
|
||||
/**
|
||||
* @param array $contactData
|
||||
*/
|
||||
public function submit(array $contactData) {
|
||||
foreach ($contactData as $field => $value) {
|
||||
$inputType = $field === 'body' ? 'textarea' : 'input';
|
||||
$this->actor->fillField($inputType . '[name="ContactForm[' . $field . ']"]', $value);
|
||||
}
|
||||
$this->actor->click('contact-button');
|
||||
}
|
||||
|
||||
}
|
24
tests/codeception/api/functional/_steps/AuthserverSteps.php
Normal file
24
tests/codeception/api/functional/_steps/AuthserverSteps.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\functional\_steps;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use tests\codeception\api\_pages\AuthserverRoute;
|
||||
use tests\codeception\api\FunctionalTester;
|
||||
|
||||
class AuthserverSteps extends FunctionalTester {
|
||||
|
||||
public function amAuthenticated() {
|
||||
$route = new AuthserverRoute($this);
|
||||
$clientToken = Uuid::uuid4()->toString();
|
||||
$route->authenticate([
|
||||
'username' => 'admin',
|
||||
'password' => 'password_0',
|
||||
'clientToken' => $clientToken,
|
||||
]);
|
||||
|
||||
$accessToken = $this->grabDataFromResponseByJsonPath('$.accessToken')[0];
|
||||
|
||||
return [$accessToken, $clientToken];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\functional\authserver;
|
||||
|
||||
use tests\codeception\api\_pages\AuthserverRoute;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use tests\codeception\api\FunctionalTester;
|
||||
|
||||
class AuthorizationCest {
|
||||
|
||||
/**
|
||||
* @var AuthserverRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new AuthserverRoute($I);
|
||||
}
|
||||
|
||||
public function byName(FunctionalTester $I) {
|
||||
$I->wantTo('authenticate by username and password');
|
||||
$this->route->authenticate([
|
||||
'username' => 'admin',
|
||||
'password' => 'password_0',
|
||||
'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 wrongArguments(FunctionalTester $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->authenticate([
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'IllegalArgumentException',
|
||||
'errorMessage' => 'credentials can not be null.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function wrongNicknameAndPassword(FunctionalTester $I) {
|
||||
$I->wantTo('authenticate by username and password with wrong data');
|
||||
$this->route->authenticate([
|
||||
'username' => 'nonexistent_user',
|
||||
'password' => 'nonexistent_password',
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Invalid credentials. Invalid nickname or password.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function bannedAccount(FunctionalTester $I) {
|
||||
$I->wantTo('authenticate in suspended account');
|
||||
$this->route->authenticate([
|
||||
'username' => 'Banned',
|
||||
'password' => 'password_0',
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'This account has been suspended.',
|
||||
]);
|
||||
}
|
||||
|
||||
private function testSuccessResponse(FunctionalTester $I) {
|
||||
$I->seeResponseCodeIs(200);
|
||||
$I->seeResponseIsJson();
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.accessToken');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.clientToken');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.availableProfiles[0].id');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.availableProfiles[0].name');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.availableProfiles[0].legacy');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.id');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.name');
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.selectedProfile.legacy');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\functional\authserver;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use tests\codeception\api\_pages\AuthserverRoute;
|
||||
use tests\codeception\api\functional\_steps\AuthserverSteps;
|
||||
|
||||
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');
|
||||
list($accessToken, $clientToken) = $I->amAuthenticated();
|
||||
$this->route->invalidate([
|
||||
'accessToken' => $accessToken,
|
||||
'clientToken' => $clientToken,
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
}
|
||||
|
||||
public function wrongArguments(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->invalidate([
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'IllegalArgumentException',
|
||||
'errorMessage' => 'credentials can not be null.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function wrongAccessTokenOrClientToken(AuthserverSteps $I) {
|
||||
$I->wantTo('invalidate by wrong client and access token');
|
||||
$this->route->invalidate([
|
||||
'accessToken' => Uuid::uuid4()->toString(),
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
}
|
||||
|
||||
}
|
77
tests/codeception/api/functional/authserver/RefreshCest.php
Normal file
77
tests/codeception/api/functional/authserver/RefreshCest.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\functional\authserver;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use tests\codeception\api\_pages\AuthserverRoute;
|
||||
use tests\codeception\api\functional\_steps\AuthserverSteps;
|
||||
|
||||
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');
|
||||
list($accessToken, $clientToken) = $I->amAuthenticated();
|
||||
$this->route->refresh([
|
||||
'accessToken' => $accessToken,
|
||||
'clientToken' => $clientToken,
|
||||
]);
|
||||
|
||||
$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 wrongArguments(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->refresh([
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'IllegalArgumentException',
|
||||
'errorMessage' => 'credentials can not be null.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function wrongAccessToken(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong access or client tokens');
|
||||
$this->route->refresh([
|
||||
'accessToken' => Uuid::uuid4()->toString(),
|
||||
'clientToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Invalid token.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function refreshTokenFromBannedUser(AuthserverSteps $I) {
|
||||
$I->wantTo('refresh token from suspended account');
|
||||
$this->route->refresh([
|
||||
'accessToken' => '918ecb41-616c-40ee-a7d2-0b0ef0d0d732',
|
||||
'clientToken' => '6042634a-a1e2-4aed-866c-c661fe4e63e2',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'This account has been suspended.',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
75
tests/codeception/api/functional/authserver/SignoutCest.php
Normal file
75
tests/codeception/api/functional/authserver/SignoutCest.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\functional\authserver;
|
||||
|
||||
use tests\codeception\api\_pages\AuthserverRoute;
|
||||
use tests\codeception\api\functional\_steps\AuthserverSteps;
|
||||
|
||||
class SignoutCest {
|
||||
|
||||
/**
|
||||
* @var AuthserverRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(AuthserverSteps $I) {
|
||||
$this->route = new AuthserverRoute($I);
|
||||
}
|
||||
|
||||
public function byName(AuthserverSteps $I) {
|
||||
$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->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
}
|
||||
|
||||
public function wrongArguments(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->signout([
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'IllegalArgumentException',
|
||||
'errorMessage' => 'credentials can not be null.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function wrongNicknameAndPassword(AuthserverSteps $I) {
|
||||
$I->wantTo('signout by nickname and password with wrong data');
|
||||
$this->route->signout([
|
||||
'username' => 'nonexistent_user',
|
||||
'password' => 'nonexistent_password',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Invalid credentials. Invalid nickname or password.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function bannedAccount(AuthserverSteps $I) {
|
||||
$I->wantTo('signout from banned account');
|
||||
$this->route->signout([
|
||||
'username' => 'Banned',
|
||||
'password' => 'password_0',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
}
|
||||
|
||||
}
|
69
tests/codeception/api/functional/authserver/ValidateCest.php
Normal file
69
tests/codeception/api/functional/authserver/ValidateCest.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\functional\authserver;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use tests\codeception\api\_pages\AuthserverRoute;
|
||||
use tests\codeception\api\functional\_steps\AuthserverSteps;
|
||||
|
||||
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');
|
||||
list($accessToken) = $I->amAuthenticated();
|
||||
$this->route->validate([
|
||||
'accessToken' => $accessToken,
|
||||
]);
|
||||
$I->seeResponseCodeIs(200);
|
||||
$I->canSeeResponseEquals('');
|
||||
}
|
||||
|
||||
public function wrongArguments(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->validate([
|
||||
'key' => 'value',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'IllegalArgumentException',
|
||||
'errorMessage' => 'credentials can not be null.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function wrongAccessToken(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on wrong accessToken');
|
||||
$this->route->validate([
|
||||
'accessToken' => Uuid::uuid4()->toString(),
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Invalid token.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function expiredAccessToken(AuthserverSteps $I) {
|
||||
$I->wantTo('get error on expired accessToken');
|
||||
$this->route->validate([
|
||||
// Заведомо истёкший токен из дампа
|
||||
'accessToken' => '6042634a-a1e2-4aed-866c-c661fe4e63e2',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Token expired.',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
@ -18,7 +18,13 @@ class ActiveUserRuleTest extends TestCase {
|
||||
$account = new AccountIdentity();
|
||||
|
||||
$this->specify('get false if user not finished registration', function() use (&$account) {
|
||||
$account->status = 0;
|
||||
$account->status = Account::STATUS_REGISTERED;
|
||||
$filter = $this->getFilterMock($account);
|
||||
expect($this->callProtected($filter, 'matchCustom', new Action(null, null)))->false();
|
||||
});
|
||||
|
||||
$this->specify('get false if user has banned status', function() use (&$account) {
|
||||
$account->status = Account::STATUS_BANNED;
|
||||
$filter = $this->getFilterMock($account);
|
||||
expect($this->callProtected($filter, 'matchCustom', new Action(null, null)))->false();
|
||||
});
|
||||
|
@ -8,7 +8,6 @@ use Codeception\Specify;
|
||||
use common\models\Account;
|
||||
use tests\codeception\api\unit\DbTestCase;
|
||||
use tests\codeception\common\fixtures\AccountFixture;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* @property AccountFixture $accounts
|
||||
@ -84,6 +83,14 @@ class LoginFormTest extends DbTestCase {
|
||||
expect($model->getErrors('login'))->equals(['error.account_not_activated']);
|
||||
});
|
||||
|
||||
$this->specify('error.account_banned if account has banned status', function () {
|
||||
$model = $this->createModel([
|
||||
'account' => new AccountIdentity(['status' => Account::STATUS_BANNED]),
|
||||
]);
|
||||
$model->validateActivity('login');
|
||||
expect($model->getErrors('login'))->equals(['error.account_banned']);
|
||||
});
|
||||
|
||||
$this->specify('no errors if account active', function () {
|
||||
$model = $this->createModel([
|
||||
'account' => new AccountIdentity(['status' => Account::STATUS_ACTIVE]),
|
||||
|
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
namespace codeception\api\unit\modules\authserver\models;
|
||||
|
||||
use api\models\AccountIdentity;
|
||||
use api\models\authentication\LoginForm;
|
||||
use api\modules\authserver\models\AuthenticateData;
|
||||
use api\modules\authserver\models\AuthenticationForm;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use tests\codeception\api\unit\DbTestCase;
|
||||
use tests\codeception\common\_support\ProtectedCaller;
|
||||
use tests\codeception\common\fixtures\AccountFixture;
|
||||
use tests\codeception\common\fixtures\MinecraftAccessKeyFixture;
|
||||
|
||||
/**
|
||||
* @property AccountFixture $accounts
|
||||
* @property MinecraftAccessKeyFixture $minecraftAccessKeys
|
||||
*/
|
||||
class AuthenticationFormTest extends DbTestCase {
|
||||
use ProtectedCaller;
|
||||
|
||||
public function fixtures() {
|
||||
return [
|
||||
'accounts' => AccountFixture::class,
|
||||
'minecraftAccessKeys' => MinecraftAccessKeyFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @expectedExceptionMessage Invalid credentials. Invalid nickname or password.
|
||||
*/
|
||||
public function testAuthenticateByWrongNicknamePass() {
|
||||
$authForm = $this->createAuthForm();
|
||||
|
||||
$authForm->username = 'wrong-username';
|
||||
$authForm->password = 'wrong-password';
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @expectedExceptionMessage Invalid credentials. Invalid email or password.
|
||||
*/
|
||||
public function testAuthenticateByWrongEmailPass() {
|
||||
$authForm = $this->createAuthForm();
|
||||
|
||||
$authForm->username = 'wrong-email@ely.by';
|
||||
$authForm->password = 'wrong-password';
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
|
||||
$authForm->authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \api\modules\authserver\exceptions\ForbiddenOperationException
|
||||
* @expectedExceptionMessage This account has been suspended.
|
||||
*/
|
||||
public function testAuthenticateByValidCredentialsIntoBlockedAccount() {
|
||||
$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')
|
||||
->will($this->returnValue($minecraftAccessKey));
|
||||
|
||||
$authForm->username = 'dummy';
|
||||
$authForm->password = 'password_0';
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
|
||||
$result = $authForm->authenticate();
|
||||
$this->assertInstanceOf(AuthenticateData::class, $result);
|
||||
$this->assertEquals($minecraftAccessKey->access_token, $result->getMinecraftAccessKey()->access_token);
|
||||
}
|
||||
|
||||
public function testCreateMinecraftAccessToken() {
|
||||
$authForm = new AuthenticationForm();
|
||||
$fixturesCount = count($this->minecraftAccessKeys->data);
|
||||
$authForm->clientToken = Uuid::uuid4();
|
||||
/** @var Account $account */
|
||||
$account = $this->accounts->getModel('admin');
|
||||
/** @var MinecraftAccessKey $result */
|
||||
$result = $this->callProtected($authForm, 'createMinecraftAccessToken', $account);
|
||||
$this->assertInstanceOf(MinecraftAccessKey::class, $result);
|
||||
$this->assertEquals($account->id, $result->account_id);
|
||||
$this->assertEquals($authForm->clientToken, $result->client_token);
|
||||
$this->assertEquals($fixturesCount + 1, MinecraftAccessKey::find()->count());
|
||||
}
|
||||
|
||||
public function testCreateMinecraftAccessTokenWithExistsClientId() {
|
||||
$authForm = new AuthenticationForm();
|
||||
$fixturesCount = count($this->minecraftAccessKeys->data);
|
||||
$authForm->clientToken = $this->minecraftAccessKeys['admin-token']['client_token'];
|
||||
/** @var Account $account */
|
||||
$account = $this->accounts->getModel('admin');
|
||||
/** @var MinecraftAccessKey $result */
|
||||
$result = $this->callProtected($authForm, 'createMinecraftAccessToken', $account);
|
||||
$this->assertInstanceOf(MinecraftAccessKey::class, $result);
|
||||
$this->assertEquals($account->id, $result->account_id);
|
||||
$this->assertEquals($authForm->clientToken, $result->client_token);
|
||||
$this->assertEquals($fixturesCount, MinecraftAccessKey::find()->count());
|
||||
}
|
||||
|
||||
private function createAuthForm($status = Account::STATUS_ACTIVE) {
|
||||
/** @var LoginForm|\PHPUnit_Framework_MockObject_MockObject $loginForm */
|
||||
$loginForm = $this->getMockBuilder(LoginForm::class)
|
||||
->setMethods(['getAccount'])
|
||||
->getMock();
|
||||
|
||||
$account = new AccountIdentity();
|
||||
$account->username = 'dummy';
|
||||
$account->email = 'dummy@ely.by';
|
||||
$account->status = $status;
|
||||
$account->setPassword('password_0');
|
||||
|
||||
$loginForm->expects($this->any())
|
||||
->method('getAccount')
|
||||
->will($this->returnValue($account));
|
||||
|
||||
/** @var AuthenticationForm|\PHPUnit_Framework_MockObject_MockObject $authForm */
|
||||
$authForm = $this->getMockBuilder(AuthenticationForm::class)
|
||||
->setMethods(['createLoginForm', 'createMinecraftAccessToken'])
|
||||
->getMock();
|
||||
|
||||
$authForm->expects($this->any())
|
||||
->method('createLoginForm')
|
||||
->will($this->returnValue($loginForm));
|
||||
|
||||
return $authForm;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace codeception\api\unit\modules\authserver\validators;
|
||||
|
||||
use api\modules\authserver\validators\RequiredValidator;
|
||||
use tests\codeception\api\unit\TestCase;
|
||||
use tests\codeception\common\_support\ProtectedCaller;
|
||||
|
||||
class RequiredValidatorTest extends TestCase {
|
||||
use ProtectedCaller;
|
||||
|
||||
public function testValidateValueNormal() {
|
||||
$validator = new RequiredValidator();
|
||||
$this->assertNull($this->callProtected($validator, 'validateValue', 'dummy'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \api\modules\authserver\exceptions\IllegalArgumentException
|
||||
*/
|
||||
public function testValidateValueEmpty() {
|
||||
$validator = new RequiredValidator();
|
||||
$this->assertNull($this->callProtected($validator, 'validateValue', ''));
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,7 @@ use Codeception\TestCase;
|
||||
use tests\codeception\common\fixtures\AccountFixture;
|
||||
use tests\codeception\common\fixtures\AccountSessionFixture;
|
||||
use tests\codeception\common\fixtures\EmailActivationFixture;
|
||||
use tests\codeception\common\fixtures\MinecraftAccessKeyFixture;
|
||||
use tests\codeception\common\fixtures\OauthClientFixture;
|
||||
use tests\codeception\common\fixtures\OauthSessionFixture;
|
||||
use tests\codeception\common\fixtures\UsernameHistoryFixture;
|
||||
@ -59,6 +60,7 @@ class FixtureHelper extends Module {
|
||||
'class' => OauthSessionFixture::class,
|
||||
'dataFile' => '@tests/codeception/common/fixtures/data/oauth-sessions.php',
|
||||
],
|
||||
'minecraftAccessKeys' => MinecraftAccessKeyFixture::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace tests\codeception\common\fixtures;
|
||||
|
||||
use common\models\MinecraftAccessKey;
|
||||
use yii\test\ActiveFixture;
|
||||
|
||||
class MinecraftAccessKeyFixture extends ActiveFixture {
|
||||
|
||||
public $modelClass = MinecraftAccessKey::class;
|
||||
|
||||
public $dataFile = '@tests/codeception/common/fixtures/data/minecraft-access-keys.php';
|
||||
|
||||
public $depends = [
|
||||
AccountFixture::class,
|
||||
];
|
||||
|
||||
}
|
@ -120,4 +120,17 @@ return [
|
||||
'created_at' => 1470499952,
|
||||
'updated_at' => 1470499952,
|
||||
],
|
||||
'banned-account' => [
|
||||
'id' => 10,
|
||||
'uuid' => 'd2e7360e-50cf-4b9b-baa0-c4440a150795',
|
||||
'username' => 'Banned',
|
||||
'email' => 'banned@ely.by',
|
||||
'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0
|
||||
'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2,
|
||||
'lang' => 'en',
|
||||
'status' => \common\models\Account::STATUS_BANNED,
|
||||
'rules_agreement_version' => \common\LATEST_RULES_VERSION,
|
||||
'created_at' => 1472682343,
|
||||
'updated_at' => 1472682343,
|
||||
],
|
||||
];
|
||||
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
return [
|
||||
'admin-token' => [
|
||||
'access_token' => 'e7bb6648-2183-4981-9b86-eba5e7f87b42',
|
||||
'client_token' => '6f380440-0c05-47bd-b7c6-d011f1b5308f',
|
||||
'account_id' => 1,
|
||||
'created_at' => time() - 10,
|
||||
'updated_at' => time() - 10,
|
||||
],
|
||||
'expired-token' => [
|
||||
'access_token' => '6042634a-a1e2-4aed-866c-c661fe4e63e2',
|
||||
'client_token' => '47fb164a-2332-42c1-8bad-549e67bb210c',
|
||||
'account_id' => 1,
|
||||
'created_at' => 1472423530,
|
||||
'updated_at' => 1472423530,
|
||||
],
|
||||
'banned-token' => [
|
||||
'access_token' => '918ecb41-616c-40ee-a7d2-0b0ef0d0d732',
|
||||
'client_token' => '6042634a-a1e2-4aed-866c-c661fe4e63e2',
|
||||
'account_id' => 10,
|
||||
'created_at' => time() - 10,
|
||||
'updated_at' => time() - 10,
|
||||
],
|
||||
];
|
@ -9,7 +9,7 @@ use yii\db\ActiveRecord;
|
||||
class PrimaryKeyValueBehaviorTest extends TestCase {
|
||||
use Specify;
|
||||
|
||||
public function testSetPrimaryKeyValue() {
|
||||
public function testRefreshPrimaryKeyValue() {
|
||||
$this->specify('method should generate value for primary key field on call', function() {
|
||||
$model = new DummyModel();
|
||||
/** @var PrimaryKeyValueBehavior|\PHPUnit_Framework_MockObject_MockObject $behavior */
|
||||
|
@ -30,5 +30,9 @@ return [
|
||||
'password' => 'tester-password',
|
||||
'vhost' => '/account.ely.by/tests',
|
||||
],
|
||||
'security' => [
|
||||
// Для тестов нам не сильно важна безопасность, а вот время прохождения тестов значительно сокращается
|
||||
'passwordHashCost' => 4,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
Loading…
x
Reference in New Issue
Block a user