mirror of
				https://github.com/elyby/accounts.git
				synced 2025-05-31 14:11:46 +05:30 
			
		
		
		
	Extract login logics into a separate component. Not quite clean result but enough for upcoming tasks
This commit is contained in:
		@@ -9,7 +9,6 @@ use Carbon\Carbon;
 | 
				
			|||||||
use common\models\Account;
 | 
					use common\models\Account;
 | 
				
			||||||
use common\models\AccountSession;
 | 
					use common\models\AccountSession;
 | 
				
			||||||
use DateTime;
 | 
					use DateTime;
 | 
				
			||||||
use Lcobucci\JWT\Token;
 | 
					 | 
				
			||||||
use Lcobucci\JWT\UnencryptedToken;
 | 
					use Lcobucci\JWT\UnencryptedToken;
 | 
				
			||||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
 | 
					use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
 | 
				
			||||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
 | 
					use League\OAuth2\Server\Entities\ScopeEntityInterface;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,35 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api\controllers;
 | 
					namespace api\controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use api\models\authentication\ForgotPasswordForm;
 | 
					use api\models\authentication\ForgotPasswordForm;
 | 
				
			||||||
use api\models\authentication\LoginForm;
 | 
					use api\models\authentication\LoginForm;
 | 
				
			||||||
use api\models\authentication\LogoutForm;
 | 
					 | 
				
			||||||
use api\models\authentication\RecoverPasswordForm;
 | 
					use api\models\authentication\RecoverPasswordForm;
 | 
				
			||||||
use api\models\authentication\RefreshTokenForm;
 | 
					use api\models\authentication\RefreshTokenForm;
 | 
				
			||||||
 | 
					use common\components\Authentication\Entities\Credentials;
 | 
				
			||||||
 | 
					use common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					use common\components\Authentication\Exceptions\AuthenticationException;
 | 
				
			||||||
 | 
					use common\components\Authentication\LoginServiceInterface;
 | 
				
			||||||
use common\helpers\Error as E;
 | 
					use common\helpers\Error as E;
 | 
				
			||||||
use common\helpers\StringHelper;
 | 
					use common\helpers\StringHelper;
 | 
				
			||||||
 | 
					use DateTimeImmutable;
 | 
				
			||||||
use Yii;
 | 
					use Yii;
 | 
				
			||||||
 | 
					use yii\base\Module;
 | 
				
			||||||
use yii\filters\AccessControl;
 | 
					use yii\filters\AccessControl;
 | 
				
			||||||
use yii\helpers\ArrayHelper;
 | 
					use yii\helpers\ArrayHelper;
 | 
				
			||||||
 | 
					use yii\web\Request;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthenticationController extends Controller {
 | 
					final class AuthenticationController extends Controller {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        string $id,
 | 
				
			||||||
 | 
					        Module $module,
 | 
				
			||||||
 | 
					        private readonly LoginServiceInterface $loginService,
 | 
				
			||||||
 | 
					        array $config = [],
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        parent::__construct($id, $module, $config);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function behaviors(): array {
 | 
					    public function behaviors(): array {
 | 
				
			||||||
        return ArrayHelper::merge(parent::behaviors(), [
 | 
					        return ArrayHelper::merge(parent::behaviors(), [
 | 
				
			||||||
@@ -38,7 +55,7 @@ class AuthenticationController extends Controller {
 | 
				
			|||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function verbs() {
 | 
					    public function verbs(): array {
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            'login' => ['POST'],
 | 
					            'login' => ['POST'],
 | 
				
			||||||
            'logout' => ['POST'],
 | 
					            'logout' => ['POST'],
 | 
				
			||||||
@@ -48,30 +65,63 @@ class AuthenticationController extends Controller {
 | 
				
			|||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function actionLogin(): array {
 | 
					    public function actionLogin(Request $request): array {
 | 
				
			||||||
        $model = new LoginForm();
 | 
					        $form = new LoginForm();
 | 
				
			||||||
        $model->load(Yii::$app->request->post());
 | 
					        $form->load($request->post());
 | 
				
			||||||
        if (($result = $model->login()) === null) {
 | 
					        if (!$form->validate()) {
 | 
				
			||||||
 | 
					            return [
 | 
				
			||||||
 | 
					                'success' => false,
 | 
				
			||||||
 | 
					                'errors' => $form->getFirstErrors(),
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $loginResult = $this->loginService->loginByCredentials(new Credentials(
 | 
				
			||||||
 | 
					                login: (string)$form->login,
 | 
				
			||||||
 | 
					                password: (string)$form->password,
 | 
				
			||||||
 | 
					                totp: (string)$form->totp,
 | 
				
			||||||
 | 
					                rememberMe: (bool)$form->rememberMe,
 | 
				
			||||||
 | 
					            ));
 | 
				
			||||||
 | 
					        } catch (AuthenticationException $e) {
 | 
				
			||||||
            $data = [
 | 
					            $data = [
 | 
				
			||||||
                'success' => false,
 | 
					                'success' => false,
 | 
				
			||||||
                'errors' => $model->getFirstErrors(),
 | 
					                'errors' => match ($e::class) {
 | 
				
			||||||
 | 
					                    Exceptions\UnknownLoginException::class => ['login' => E::LOGIN_NOT_EXIST],
 | 
				
			||||||
 | 
					                    Exceptions\InvalidPasswordException::class => ['password' => E::PASSWORD_INCORRECT],
 | 
				
			||||||
 | 
					                    Exceptions\TotpRequiredException::class => ['totp' => E::TOTP_REQUIRED],
 | 
				
			||||||
 | 
					                    Exceptions\InvalidTotpException::class => ['totp' => E::TOTP_INCORRECT],
 | 
				
			||||||
 | 
					                    Exceptions\AccountBannedException::class => ['login' => E::ACCOUNT_BANNED],
 | 
				
			||||||
 | 
					                    Exceptions\AccountNotActivatedException::class => ['login' => E::ACCOUNT_NOT_ACTIVATED],
 | 
				
			||||||
 | 
					                    default => $e->getMessage(),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
            ];
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (ArrayHelper::getValue($data['errors'], 'login') === E::ACCOUNT_NOT_ACTIVATED) {
 | 
					            if ($e instanceof Exceptions\AccountNotActivatedException) {
 | 
				
			||||||
                $data['data']['email'] = $model->getAccount()->email;
 | 
					                $data['data']['email'] = $e->account->email;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return $data;
 | 
					            return $data;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return array_merge([
 | 
					        $token = Yii::$app->tokensFactory->createForWebAccount($loginResult->account, $loginResult->session);
 | 
				
			||||||
 | 
					        $data = [
 | 
				
			||||||
            'success' => true,
 | 
					            'success' => true,
 | 
				
			||||||
        ], $result->formatAsOAuth2Response());
 | 
					            'access_token' => $token->toString(),
 | 
				
			||||||
 | 
					            'expires_in' => $token->claims()->get('exp')->getTimestamp() - (new DateTimeImmutable())->getTimestamp(),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($loginResult->session) {
 | 
				
			||||||
 | 
					            $data['refresh_token'] = $loginResult->session->refresh_token;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $data;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function actionLogout(): array {
 | 
					    public function actionLogout(): array {
 | 
				
			||||||
        $form = new LogoutForm();
 | 
					        $session = Yii::$app->user->getActiveSession();
 | 
				
			||||||
        $form->logout();
 | 
					        if ($session) {
 | 
				
			||||||
 | 
					            $this->loginService->logout($session);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            'success' => true,
 | 
					            'success' => true,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@ namespace api\eventListeners;
 | 
				
			|||||||
use api\controllers\AuthenticationController;
 | 
					use api\controllers\AuthenticationController;
 | 
				
			||||||
use api\controllers\SignupController;
 | 
					use api\controllers\SignupController;
 | 
				
			||||||
use api\modules\accounts\actions;
 | 
					use api\modules\accounts\actions;
 | 
				
			||||||
use Closure;
 | 
					 | 
				
			||||||
use Yii;
 | 
					use Yii;
 | 
				
			||||||
use yii\base\ActionEvent;
 | 
					use yii\base\ActionEvent;
 | 
				
			||||||
use yii\base\BootstrapInterface;
 | 
					use yii\base\BootstrapInterface;
 | 
				
			||||||
@@ -16,8 +15,8 @@ use yii\base\Event;
 | 
				
			|||||||
final class LogMetricsToStatsd implements BootstrapInterface {
 | 
					final class LogMetricsToStatsd implements BootstrapInterface {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function bootstrap($app): void {
 | 
					    public function bootstrap($app): void {
 | 
				
			||||||
        Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, Closure::fromCallable([$this, 'beforeAction']));
 | 
					        Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, $this->beforeAction(...));
 | 
				
			||||||
        Event::on(Controller::class, Controller::EVENT_AFTER_ACTION, Closure::fromCallable([$this, 'afterAction']));
 | 
					        Event::on(Controller::class, Controller::EVENT_AFTER_ACTION, $this->afterAction(...));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function beforeAction(ActionEvent $event): void {
 | 
					    private function beforeAction(ActionEvent $event): void {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ namespace api\models\authentication;
 | 
				
			|||||||
use DateTimeImmutable;
 | 
					use DateTimeImmutable;
 | 
				
			||||||
use Lcobucci\JWT\UnencryptedToken;
 | 
					use Lcobucci\JWT\UnencryptedToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: remove this class
 | 
				
			||||||
final readonly class AuthenticationResult {
 | 
					final readonly class AuthenticationResult {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function __construct(
 | 
					    public function __construct(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,14 +4,9 @@ declare(strict_types=1);
 | 
				
			|||||||
namespace api\models\authentication;
 | 
					namespace api\models\authentication;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use api\models\base\ApiForm;
 | 
					use api\models\base\ApiForm;
 | 
				
			||||||
use api\validators\TotpValidator;
 | 
					 | 
				
			||||||
use common\helpers\Error as E;
 | 
					use common\helpers\Error as E;
 | 
				
			||||||
use common\models\Account;
 | 
					 | 
				
			||||||
use common\models\AccountSession;
 | 
					 | 
				
			||||||
use Webmozart\Assert\Assert;
 | 
					 | 
				
			||||||
use Yii;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LoginForm extends ApiForm {
 | 
					final class LoginForm extends ApiForm {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public mixed $login = null;
 | 
					    public mixed $login = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -24,97 +19,10 @@ class LoginForm extends ApiForm {
 | 
				
			|||||||
    public function rules(): array {
 | 
					    public function rules(): array {
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            ['login', 'required', 'message' => E::LOGIN_REQUIRED],
 | 
					            ['login', 'required', 'message' => E::LOGIN_REQUIRED],
 | 
				
			||||||
            ['login', 'validateLogin'],
 | 
					            ['password', 'required', 'isEmpty' => fn($value) => $value === null, 'message' => E::PASSWORD_REQUIRED],
 | 
				
			||||||
 | 
					            ['totp', 'string'],
 | 
				
			||||||
            ['password', 'required', 'when' => fn(self $model): bool => !$model->hasErrors(), 'message' => E::PASSWORD_REQUIRED],
 | 
					 | 
				
			||||||
            ['password', 'validatePassword'],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ['totp', 'required', 'when' => fn(self $model): bool => !$model->hasErrors() && $model->getAccount()->is_otp_enabled, 'message' => E::TOTP_REQUIRED],
 | 
					 | 
				
			||||||
            ['totp', 'validateTotp'],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ['login', 'validateActivity'],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            ['rememberMe', 'boolean'],
 | 
					            ['rememberMe', 'boolean'],
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function validateLogin(string $attribute): void {
 | 
					 | 
				
			||||||
        if (!$this->hasErrors() && $this->getAccount() === null) {
 | 
					 | 
				
			||||||
            $this->addError($attribute, E::LOGIN_NOT_EXIST);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function validatePassword(string $attribute): void {
 | 
					 | 
				
			||||||
        if (!$this->hasErrors()) {
 | 
					 | 
				
			||||||
            $account = $this->getAccount();
 | 
					 | 
				
			||||||
            if ($account === null || !$account->validatePassword($this->password)) {
 | 
					 | 
				
			||||||
                $this->addError($attribute, E::PASSWORD_INCORRECT);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function validateTotp(string $attribute): void {
 | 
					 | 
				
			||||||
        if ($this->hasErrors()) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /** @var Account $account */
 | 
					 | 
				
			||||||
        $account = $this->getAccount();
 | 
					 | 
				
			||||||
        if (!$account->is_otp_enabled) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $validator = new TotpValidator(['account' => $account]);
 | 
					 | 
				
			||||||
        $validator->validateAttribute($this, $attribute);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function validateActivity(string $attribute): void {
 | 
					 | 
				
			||||||
        if (!$this->hasErrors()) {
 | 
					 | 
				
			||||||
            /** @var Account $account */
 | 
					 | 
				
			||||||
            $account = $this->getAccount();
 | 
					 | 
				
			||||||
            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);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** @noinspection PhpIncompatibleReturnTypeInspection */
 | 
					 | 
				
			||||||
    public function getAccount(): ?Account {
 | 
					 | 
				
			||||||
        return Account::find()->andWhereLogin($this->login)->one();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function login(): ?AuthenticationResult {
 | 
					 | 
				
			||||||
        if (!$this->validate()) {
 | 
					 | 
				
			||||||
            return null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $transaction = Yii::$app->db->beginTransaction();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /** @var Account $account */
 | 
					 | 
				
			||||||
        $account = $this->getAccount();
 | 
					 | 
				
			||||||
        if ($account->password_hash_strategy !== Account::PASS_HASH_STRATEGY_YII2) {
 | 
					 | 
				
			||||||
            $account->setPassword($this->password);
 | 
					 | 
				
			||||||
            Assert::true($account->save(), 'Unable to upgrade user\'s password');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $session = null;
 | 
					 | 
				
			||||||
        if ($this->rememberMe) {
 | 
					 | 
				
			||||||
            $session = new AccountSession();
 | 
					 | 
				
			||||||
            $session->account_id = $account->id;
 | 
					 | 
				
			||||||
            $session->setIp(Yii::$app->request->userIP);
 | 
					 | 
				
			||||||
            $session->generateRefreshToken();
 | 
					 | 
				
			||||||
            Assert::true($session->save(), 'Cannot save account session model');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $token = Yii::$app->tokensFactory->createForWebAccount($account, $session);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $transaction->commit();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return new AuthenticationResult($token, $session?->refresh_token);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
declare(strict_types=1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace api\models\authentication;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use api\models\base\ApiForm;
 | 
					 | 
				
			||||||
use Yii;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LogoutForm extends ApiForm {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function logout(): bool {
 | 
					 | 
				
			||||||
        $component = Yii::$app->user;
 | 
					 | 
				
			||||||
        $session = $component->getActiveSession();
 | 
					 | 
				
			||||||
        if ($session === null) {
 | 
					 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $session->delete();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -5,7 +5,7 @@ namespace api\models\base;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use yii\base\Model;
 | 
					use yii\base\Model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApiForm extends Model {
 | 
					abstract class ApiForm extends Model {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function formName(): string {
 | 
					    public function formName(): string {
 | 
				
			||||||
        return '';
 | 
					        return '';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,6 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api\modules\accounts\models;
 | 
					namespace api\modules\accounts\models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use api\validators\PasswordRequiredValidator;
 | 
					use api\validators\PasswordRequiredValidator;
 | 
				
			||||||
@@ -8,13 +10,13 @@ use Webmozart\Assert\Assert;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class DisableTwoFactorAuthForm extends AccountActionForm {
 | 
					class DisableTwoFactorAuthForm extends AccountActionForm {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public $totp;
 | 
					    public mixed $totp = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public $password;
 | 
					    public mixed $password = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function rules(): array {
 | 
					    public function rules(): array {
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            ['account', 'validateOtpEnabled'],
 | 
					            ['account', $this->validateOtpEnabled(...)],
 | 
				
			||||||
            ['totp', 'required', 'message' => E::TOTP_REQUIRED],
 | 
					            ['totp', 'required', 'message' => E::TOTP_REQUIRED],
 | 
				
			||||||
            ['totp', TotpValidator::class, 'account' => $this->getAccount()],
 | 
					            ['totp', TotpValidator::class, 'account' => $this->getAccount()],
 | 
				
			||||||
            ['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
 | 
					            ['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
 | 
				
			||||||
@@ -34,7 +36,7 @@ class DisableTwoFactorAuthForm extends AccountActionForm {
 | 
				
			|||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function validateOtpEnabled($attribute): void {
 | 
					    private function validateOtpEnabled(string $attribute): void {
 | 
				
			||||||
        if (!$this->getAccount()->is_otp_enabled) {
 | 
					        if (!$this->getAccount()->is_otp_enabled) {
 | 
				
			||||||
            $this->addError($attribute, E::OTP_NOT_ENABLED);
 | 
					            $this->addError($attribute, E::OTP_NOT_ENABLED);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,6 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api\modules\accounts\models;
 | 
					namespace api\modules\accounts\models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use api\components\User\Component;
 | 
					use api\components\User\Component;
 | 
				
			||||||
@@ -10,13 +12,13 @@ use Yii;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class EnableTwoFactorAuthForm extends AccountActionForm {
 | 
					class EnableTwoFactorAuthForm extends AccountActionForm {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public $totp;
 | 
					    public mixed $totp = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public $password;
 | 
					    public mixed $password = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function rules(): array {
 | 
					    public function rules(): array {
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            ['account', 'validateOtpDisabled'],
 | 
					            ['account', $this->validateOtpDisabled(...)],
 | 
				
			||||||
            ['totp', 'required', 'message' => E::TOTP_REQUIRED],
 | 
					            ['totp', 'required', 'message' => E::TOTP_REQUIRED],
 | 
				
			||||||
            ['totp', TotpValidator::class, 'account' => $this->getAccount()],
 | 
					            ['totp', TotpValidator::class, 'account' => $this->getAccount()],
 | 
				
			||||||
            ['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
 | 
					            ['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
 | 
				
			||||||
@@ -41,7 +43,7 @@ class EnableTwoFactorAuthForm extends AccountActionForm {
 | 
				
			|||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function validateOtpDisabled($attribute): void {
 | 
					    private function validateOtpDisabled(string $attribute): void {
 | 
				
			||||||
        if ($this->getAccount()->is_otp_enabled) {
 | 
					        if ($this->getAccount()->is_otp_enabled) {
 | 
				
			||||||
            $this->addError($attribute, E::OTP_ALREADY_ENABLED);
 | 
					            $this->addError($attribute, E::OTP_ALREADY_ENABLED);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ use api\controllers\Controller;
 | 
				
			|||||||
use api\modules\authserver\models;
 | 
					use api\modules\authserver\models;
 | 
				
			||||||
use Yii;
 | 
					use Yii;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthenticationController extends Controller {
 | 
					final class AuthenticationController extends Controller {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function behaviors(): array {
 | 
					    public function behaviors(): array {
 | 
				
			||||||
        $behaviors = parent::behaviors();
 | 
					        $behaviors = parent::behaviors();
 | 
				
			||||||
@@ -27,12 +27,11 @@ class AuthenticationController extends Controller {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @return array
 | 
					 | 
				
			||||||
     * @throws \api\modules\authserver\exceptions\ForbiddenOperationException
 | 
					     * @throws \api\modules\authserver\exceptions\ForbiddenOperationException
 | 
				
			||||||
     * @throws \api\modules\authserver\exceptions\IllegalArgumentException
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function actionAuthenticate(): array {
 | 
					    public function actionAuthenticate(): array {
 | 
				
			||||||
        $model = new models\AuthenticationForm();
 | 
					        /** @var \api\modules\authserver\models\AuthenticationForm $model */
 | 
				
			||||||
 | 
					        $model = Yii::createObject(models\AuthenticationForm::class);
 | 
				
			||||||
        $model->load(Yii::$app->request->post());
 | 
					        $model->load(Yii::$app->request->post());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $model->authenticate()->getResponseData(true);
 | 
					        return $model->authenticate()->getResponseData(true);
 | 
				
			||||||
@@ -62,10 +61,6 @@ class AuthenticationController extends Controller {
 | 
				
			|||||||
        // In case of an error, an exception is thrown which will be processed by ErrorHandler
 | 
					        // In case of an error, an exception is thrown which will be processed by ErrorHandler
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @throws \api\modules\authserver\exceptions\ForbiddenOperationException
 | 
					 | 
				
			||||||
     * @throws \api\modules\authserver\exceptions\IllegalArgumentException
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function actionSignout(): void {
 | 
					    public function actionSignout(): void {
 | 
				
			||||||
        $model = new models\SignoutForm();
 | 
					        $model = new models\SignoutForm();
 | 
				
			||||||
        $model->load(Yii::$app->request->post());
 | 
					        $model->load(Yii::$app->request->post());
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,28 @@ final readonly class AuthenticateData {
 | 
				
			|||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @return array{
 | 
				
			||||||
 | 
					     *     accessToken: string,
 | 
				
			||||||
 | 
					     *     clientToken: string,
 | 
				
			||||||
 | 
					     *     selectedProfile: array{
 | 
				
			||||||
 | 
					     *         id: string,
 | 
				
			||||||
 | 
					     *         name: string,
 | 
				
			||||||
 | 
					     *     },
 | 
				
			||||||
 | 
					     *     availableProfiles?: array<array{
 | 
				
			||||||
 | 
					     *         id: string,
 | 
				
			||||||
 | 
					     *         name: string,
 | 
				
			||||||
 | 
					     *     }>,
 | 
				
			||||||
 | 
					     *     user?: array{
 | 
				
			||||||
 | 
					     *         id: string,
 | 
				
			||||||
 | 
					     *         username: string,
 | 
				
			||||||
 | 
					     *         properties: array<array{
 | 
				
			||||||
 | 
					     *             name: string,
 | 
				
			||||||
 | 
					     *             value: string,
 | 
				
			||||||
 | 
					     *         }>,
 | 
				
			||||||
 | 
					     *     },
 | 
				
			||||||
 | 
					     * }
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
    public function getResponseData(bool $includeAvailableProfiles = false): array {
 | 
					    public function getResponseData(bool $includeAvailableProfiles = false): array {
 | 
				
			||||||
        $uuid = str_replace('-', '', $this->account->uuid);
 | 
					        $uuid = str_replace('-', '', $this->account->uuid);
 | 
				
			||||||
        $result = [
 | 
					        $result = [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,43 +3,40 @@ declare(strict_types=1);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace api\modules\authserver\models;
 | 
					namespace api\modules\authserver\models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use api\models\authentication\LoginForm;
 | 
					use api\components\Tokens\TokensFactory;
 | 
				
			||||||
use api\models\base\ApiForm;
 | 
					use api\models\base\ApiForm;
 | 
				
			||||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
 | 
					use api\modules\authserver\exceptions\ForbiddenOperationException;
 | 
				
			||||||
use api\modules\authserver\Module as Authserver;
 | 
					use api\modules\authserver\Module as Authserver;
 | 
				
			||||||
use api\modules\authserver\validators\ClientTokenValidator;
 | 
					use api\modules\authserver\validators\ClientTokenValidator;
 | 
				
			||||||
use api\modules\authserver\validators\RequiredValidator;
 | 
					use api\modules\authserver\validators\RequiredValidator;
 | 
				
			||||||
use api\rbac\Permissions as P;
 | 
					use api\rbac\Permissions as P;
 | 
				
			||||||
use common\helpers\Error as E;
 | 
					use common\components\Authentication\Entities\Credentials;
 | 
				
			||||||
 | 
					use common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					use common\components\Authentication\Exceptions\AuthenticationException;
 | 
				
			||||||
 | 
					use common\components\Authentication\LoginServiceInterface;
 | 
				
			||||||
use common\models\Account;
 | 
					use common\models\Account;
 | 
				
			||||||
use common\models\OauthClient;
 | 
					use common\models\OauthClient;
 | 
				
			||||||
use common\models\OauthSession;
 | 
					use common\models\OauthSession;
 | 
				
			||||||
use Ramsey\Uuid\Uuid;
 | 
					use Ramsey\Uuid\Uuid;
 | 
				
			||||||
use Webmozart\Assert\Assert;
 | 
					use Webmozart\Assert\Assert;
 | 
				
			||||||
use Yii;
 | 
					 | 
				
			||||||
use yii\db\Exception;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthenticationForm extends ApiForm {
 | 
					final class AuthenticationForm extends ApiForm {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    public mixed $username = null;
 | 
				
			||||||
     * @var string
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $username;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    public mixed $password = null;
 | 
				
			||||||
     * @var string
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $password;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    public mixed $clientToken = null;
 | 
				
			||||||
     * @var string
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $clientToken;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    public mixed $requestUser = null;
 | 
				
			||||||
     * @var string|bool
 | 
					
 | 
				
			||||||
     */
 | 
					    public function __construct(
 | 
				
			||||||
    public $requestUser;
 | 
					        private readonly LoginServiceInterface $loginService,
 | 
				
			||||||
 | 
					        private readonly TokensFactory $tokensFactory,
 | 
				
			||||||
 | 
					        array $config = [],
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        parent::__construct($config);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function rules(): array {
 | 
					    public function rules(): array {
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
@@ -50,9 +47,7 @@ class AuthenticationForm extends ApiForm {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @return AuthenticateData
 | 
					 | 
				
			||||||
     * @throws ForbiddenOperationException
 | 
					     * @throws ForbiddenOperationException
 | 
				
			||||||
     * @throws Exception
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function authenticate(): AuthenticateData {
 | 
					    public function authenticate(): AuthenticateData {
 | 
				
			||||||
        // This validating method will throw an exception in case when validation will not pass successfully
 | 
					        // This validating method will throw an exception in case when validation will not pass successfully
 | 
				
			||||||
@@ -60,11 +55,7 @@ class AuthenticationForm extends ApiForm {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Authserver::info("Trying to authenticate user by login = '{$this->username}'.");
 | 
					        Authserver::info("Trying to authenticate user by login = '{$this->username}'.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // The previous authorization server implementation used the nickname field instead of username,
 | 
					        $password = (string)$this->password;
 | 
				
			||||||
        // so we keep such behavior
 | 
					 | 
				
			||||||
        $attribute = !str_contains($this->username, '@') ? 'nickname' : 'email';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $password = $this->password;
 | 
					 | 
				
			||||||
        $totp = null;
 | 
					        $totp = null;
 | 
				
			||||||
        if (preg_match('/.{8,}:(\d{6})$/', $password, $matches) === 1) {
 | 
					        if (preg_match('/.{8,}:(\d{6})$/', $password, $matches) === 1) {
 | 
				
			||||||
            $totp = $matches[1];
 | 
					            $totp = $matches[1];
 | 
				
			||||||
@@ -73,47 +64,32 @@ class AuthenticationForm extends ApiForm {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        login:
 | 
					        login:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $loginForm = new LoginForm();
 | 
					        $credentials = new Credentials(
 | 
				
			||||||
        $loginForm->login = $this->username;
 | 
					            login: (string)$this->username,
 | 
				
			||||||
        $loginForm->password = $password;
 | 
					            password: $password,
 | 
				
			||||||
        $loginForm->totp = $totp;
 | 
					            totp: $totp,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $isValid = $loginForm->validate();
 | 
					        try {
 | 
				
			||||||
        // Handle case when user's password matches the template for totp via password
 | 
					            $result = $this->loginService->loginByCredentials($credentials);
 | 
				
			||||||
        if (!$isValid && $totp !== null && $loginForm->getFirstError('password') === E::PASSWORD_INCORRECT) {
 | 
					        } catch (Exceptions\InvalidPasswordException $e) {
 | 
				
			||||||
            $password = "{$password}:{$totp}";
 | 
					            if ($totp !== null) {
 | 
				
			||||||
            $totp = null;
 | 
					                $password = $this->password;
 | 
				
			||||||
 | 
					                goto login;
 | 
				
			||||||
            goto login;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!$isValid || $loginForm->getAccount()->status === Account::STATUS_DELETED) {
 | 
					 | 
				
			||||||
            $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.');
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Authserver::error("Cannot find user by login = '{$this->username}'");
 | 
					 | 
				
			||||||
            } elseif (isset($errors['password'])) {
 | 
					 | 
				
			||||||
                Authserver::error("User with login = '{$this->username}' passed wrong password.");
 | 
					 | 
				
			||||||
            } elseif (isset($errors['totp'])) {
 | 
					 | 
				
			||||||
                if ($errors['totp'] === E::TOTP_REQUIRED) {
 | 
					 | 
				
			||||||
                    Authserver::error("User with login = '{$this->username}' protected by two factor auth.");
 | 
					 | 
				
			||||||
                    throw new ForbiddenOperationException('Account protected with two factor auth.');
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Authserver::error("User with login = '{$this->username}' passed wrong totp token");
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            throw new ForbiddenOperationException("Invalid credentials. Invalid {$attribute} or password.");
 | 
					            $this->convertAuthenticationException($e);
 | 
				
			||||||
 | 
					        } catch (AuthenticationException $e) {
 | 
				
			||||||
 | 
					            $this->convertAuthenticationException($e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $account = $result->account;
 | 
				
			||||||
 | 
					        if ($account->status === Account::STATUS_DELETED) {
 | 
				
			||||||
 | 
					            throw new ForbiddenOperationException('Invalid credentials. Invalid username or password.');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /** @var Account $account */
 | 
					 | 
				
			||||||
        $account = $loginForm->getAccount();
 | 
					 | 
				
			||||||
        $clientToken = $this->clientToken ?: Uuid::uuid4()->toString();
 | 
					        $clientToken = $this->clientToken ?: Uuid::uuid4()->toString();
 | 
				
			||||||
        $token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $clientToken);
 | 
					        $token = $this->tokensFactory->createForMinecraftAccount($account, $clientToken);
 | 
				
			||||||
        $dataModel = new AuthenticateData($account, $token->toString(), $clientToken, (bool)$this->requestUser);
 | 
					        $dataModel = new AuthenticateData($account, $token->toString(), $clientToken, (bool)$this->requestUser);
 | 
				
			||||||
        /** @var OauthSession|null $minecraftOauthSession */
 | 
					        /** @var OauthSession|null $minecraftOauthSession */
 | 
				
			||||||
        $minecraftOauthSession = $account->getOauthSessions()
 | 
					        $minecraftOauthSession = $account->getOauthSessions()
 | 
				
			||||||
@@ -134,4 +110,15 @@ class AuthenticationForm extends ApiForm {
 | 
				
			|||||||
        return $dataModel;
 | 
					        return $dataModel;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @throws \api\modules\authserver\exceptions\ForbiddenOperationException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private function convertAuthenticationException(AuthenticationException $e): never {
 | 
				
			||||||
 | 
					        throw match ($e::class) {
 | 
				
			||||||
 | 
					            Exceptions\AccountBannedException::class => new ForbiddenOperationException('This account has been suspended.'),
 | 
				
			||||||
 | 
					            Exceptions\TotpRequiredException::class => new ForbiddenOperationException('Account protected with two factor auth.'),
 | 
				
			||||||
 | 
					            default => new ForbiddenOperationException('Invalid credentials. Invalid username or password.'),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,23 +3,14 @@ declare(strict_types=1);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace api\modules\authserver\models;
 | 
					namespace api\modules\authserver\models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use api\models\authentication\LoginForm;
 | 
					 | 
				
			||||||
use api\models\base\ApiForm;
 | 
					use api\models\base\ApiForm;
 | 
				
			||||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
 | 
					 | 
				
			||||||
use api\modules\authserver\validators\RequiredValidator;
 | 
					use api\modules\authserver\validators\RequiredValidator;
 | 
				
			||||||
use common\helpers\Error as E;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SignoutForm extends ApiForm {
 | 
					final class SignoutForm extends ApiForm {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    public mixed $username = null;
 | 
				
			||||||
     * @var string
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $username;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    public mixed $password = null;
 | 
				
			||||||
     * @var string
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public $password;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function rules(): array {
 | 
					    public function rules(): array {
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
@@ -27,32 +18,11 @@ class SignoutForm extends ApiForm {
 | 
				
			|||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @return bool
 | 
					 | 
				
			||||||
     * @throws ForbiddenOperationException
 | 
					 | 
				
			||||||
     * @throws \api\modules\authserver\exceptions\IllegalArgumentException
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function signout(): bool {
 | 
					    public function signout(): bool {
 | 
				
			||||||
        $this->validate();
 | 
					        $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) {
 | 
					 | 
				
			||||||
                // We believe that a blocked one can get out painlessly
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // The previous authorization server implementation used the nickname field instead of username,
 | 
					 | 
				
			||||||
            // so we keep such behavior
 | 
					 | 
				
			||||||
            $attribute = !str_contains($this->username, '@') ? 'nickname' : 'email';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            throw new ForbiddenOperationException("Invalid credentials. Invalid {$attribute} or password.");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // We're unable to invalidate access tokens because they aren't stored in our database
 | 
					        // We're unable to invalidate access tokens because they aren't stored in our database
 | 
				
			||||||
 | 
					        // We don't give an error about invalid credentials to eliminate a point through which attackers can brut force passwords.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,10 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api\tests\_pages;
 | 
					namespace api\tests\_pages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthenticationRoute extends BasePage {
 | 
					final class AuthenticationRoute extends BasePage {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @param string $login
 | 
					 | 
				
			||||||
     * @param string $password
 | 
					 | 
				
			||||||
     * @param bool|string|null $rememberMeOrToken
 | 
					 | 
				
			||||||
     * @param bool $rememberMe
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function login(string $login = '', string $password = '', bool|string|null $rememberMeOrToken = null, bool $rememberMe = false): void {
 | 
					    public function login(string $login = '', string $password = '', bool|string|null $rememberMeOrToken = null, bool $rememberMe = false): void {
 | 
				
			||||||
        $params = [
 | 
					        $params = [
 | 
				
			||||||
            'login' => $login,
 | 
					            'login' => $login,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ use api\tests\FunctionalTester;
 | 
				
			|||||||
use OTPHP\TOTP;
 | 
					use OTPHP\TOTP;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: very outdated tests. Need to rewrite
 | 
					// TODO: very outdated tests. Need to rewrite
 | 
				
			||||||
class LoginCest {
 | 
					final class LoginCest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testLoginEmailOrUsername(FunctionalTester $I): void {
 | 
					    public function testLoginEmailOrUsername(FunctionalTester $I): void {
 | 
				
			||||||
        $route = new AuthenticationRoute($I);
 | 
					        $route = new AuthenticationRoute($I);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -156,7 +156,7 @@ class AuthorizationCest {
 | 
				
			|||||||
        $I->canSeeResponseIsJson();
 | 
					        $I->canSeeResponseIsJson();
 | 
				
			||||||
        $I->canSeeResponseContainsJson([
 | 
					        $I->canSeeResponseContainsJson([
 | 
				
			||||||
            'error' => 'ForbiddenOperationException',
 | 
					            'error' => 'ForbiddenOperationException',
 | 
				
			||||||
            'errorMessage' => 'Invalid credentials. Invalid nickname or password.',
 | 
					            'errorMessage' => 'Invalid credentials. Invalid username or password.',
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -170,7 +170,7 @@ class AuthorizationCest {
 | 
				
			|||||||
        $I->canSeeResponseCodeIs(401);
 | 
					        $I->canSeeResponseCodeIs(401);
 | 
				
			||||||
        $I->canSeeResponseContainsJson([
 | 
					        $I->canSeeResponseContainsJson([
 | 
				
			||||||
            'error' => 'ForbiddenOperationException',
 | 
					            'error' => 'ForbiddenOperationException',
 | 
				
			||||||
            'errorMessage' => 'Invalid credentials. Invalid nickname or password.',
 | 
					            'errorMessage' => 'Invalid credentials. Invalid username or password.',
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,14 +6,15 @@ namespace api\tests\functional\authserver;
 | 
				
			|||||||
use api\tests\functional\_steps\AuthserverSteps;
 | 
					use api\tests\functional\_steps\AuthserverSteps;
 | 
				
			||||||
use Codeception\Example;
 | 
					use Codeception\Example;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SignoutCest {
 | 
					final class SignoutCest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @example {"login": "admin", "password": "password_0"}
 | 
					     * @example {"login": "admin", "password": "password_0"}
 | 
				
			||||||
     * @example {"login": "admin@ely.by", "password": "password_0"}
 | 
					     * @example {"login": "admin@ely.by", "password": "password_0"}
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param \Codeception\Example<array{login: string, password: string}> $example
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function signout(AuthserverSteps $I, Example $example): void {
 | 
					    public function signout(AuthserverSteps $I, Example $example): void {
 | 
				
			||||||
        $I->wantTo('signout by nickname and password');
 | 
					 | 
				
			||||||
        $I->sendPOST('/api/authserver/authentication/signout', [
 | 
					        $I->sendPOST('/api/authserver/authentication/signout', [
 | 
				
			||||||
            'username' => $example['login'],
 | 
					            'username' => $example['login'],
 | 
				
			||||||
            'password' => $example['password'],
 | 
					            'password' => $example['password'],
 | 
				
			||||||
@@ -23,7 +24,6 @@ class SignoutCest {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function wrongArguments(AuthserverSteps $I): void {
 | 
					    public function wrongArguments(AuthserverSteps $I): void {
 | 
				
			||||||
        $I->wantTo('get error on wrong amount of arguments');
 | 
					 | 
				
			||||||
        $I->sendPOST('/api/authserver/authentication/signout', [
 | 
					        $I->sendPOST('/api/authserver/authentication/signout', [
 | 
				
			||||||
            'key' => 'value',
 | 
					            'key' => 'value',
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
@@ -36,21 +36,15 @@ class SignoutCest {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function wrongNicknameAndPassword(AuthserverSteps $I): void {
 | 
					    public function wrongNicknameAndPassword(AuthserverSteps $I): void {
 | 
				
			||||||
        $I->wantTo('signout by nickname and password with wrong data');
 | 
					 | 
				
			||||||
        $I->sendPOST('/api/authserver/authentication/signout', [
 | 
					        $I->sendPOST('/api/authserver/authentication/signout', [
 | 
				
			||||||
            'username' => 'nonexistent_user',
 | 
					            'username' => 'nonexistent_user',
 | 
				
			||||||
            'password' => 'nonexistent_password',
 | 
					            'password' => 'nonexistent_password',
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        $I->canSeeResponseCodeIs(401);
 | 
					        $I->canSeeResponseCodeIs(200);
 | 
				
			||||||
        $I->canSeeResponseIsJson();
 | 
					        $I->canSeeResponseEquals('');
 | 
				
			||||||
        $I->canSeeResponseContainsJson([
 | 
					 | 
				
			||||||
            'error' => 'ForbiddenOperationException',
 | 
					 | 
				
			||||||
            'errorMessage' => 'Invalid credentials. Invalid nickname or password.',
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function bannedAccount(AuthserverSteps $I): void {
 | 
					    public function bannedAccount(AuthserverSteps $I): void {
 | 
				
			||||||
        $I->wantTo('signout from banned account');
 | 
					 | 
				
			||||||
        $I->sendPOST('/api/authserver/authentication/signout', [
 | 
					        $I->sendPOST('/api/authserver/authentication/signout', [
 | 
				
			||||||
            'username' => 'Banned',
 | 
					            'username' => 'Banned',
 | 
				
			||||||
            'password' => 'password_0',
 | 
					            'password' => 'password_0',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,122 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
declare(strict_types=1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace api\tests\unit\models\authentication;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use api\models\authentication\LoginForm;
 | 
					 | 
				
			||||||
use api\tests\unit\TestCase;
 | 
					 | 
				
			||||||
use common\models\Account;
 | 
					 | 
				
			||||||
use common\tests\fixtures\AccountFixture;
 | 
					 | 
				
			||||||
use OTPHP\TOTP;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LoginFormTest extends TestCase {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function _fixtures(): array {
 | 
					 | 
				
			||||||
        return [
 | 
					 | 
				
			||||||
            'accounts' => AccountFixture::class,
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testValidateLogin(): void {
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount(null);
 | 
					 | 
				
			||||||
        $model->login = 'mock-login';
 | 
					 | 
				
			||||||
        $model->validateLogin('login');
 | 
					 | 
				
			||||||
        $this->assertSame(['error.login_not_exist'], $model->getErrors('login'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount(new Account());
 | 
					 | 
				
			||||||
        $model->login = 'mock-login';
 | 
					 | 
				
			||||||
        $model->validateLogin('login');
 | 
					 | 
				
			||||||
        $this->assertEmpty($model->getErrors('login'));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testValidatePassword(): void {
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->password_hash = '$2y$04$N0q8DaHzlYILCnLYrpZfEeWKEqkPZzbawiS07GbSr/.xbRNweSLU6'; // 12345678
 | 
					 | 
				
			||||||
        $account->password_hash_strategy = Account::PASS_HASH_STRATEGY_YII2;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->password = '87654321';
 | 
					 | 
				
			||||||
        $model->validatePassword('password');
 | 
					 | 
				
			||||||
        $this->assertSame(['error.password_incorrect'], $model->getErrors('password'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->password = '12345678';
 | 
					 | 
				
			||||||
        $model->validatePassword('password');
 | 
					 | 
				
			||||||
        $this->assertEmpty($model->getErrors('password'));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testValidateTotp(): void {
 | 
					 | 
				
			||||||
        $account = new Account(['password' => '12345678']);
 | 
					 | 
				
			||||||
        $account->password = '12345678';
 | 
					 | 
				
			||||||
        $account->is_otp_enabled = true;
 | 
					 | 
				
			||||||
        $account->otp_secret = 'AAAA';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->password = '12345678';
 | 
					 | 
				
			||||||
        $model->totp = '321123';
 | 
					 | 
				
			||||||
        $model->validateTotp('totp');
 | 
					 | 
				
			||||||
        $this->assertSame(['error.totp_incorrect'], $model->getErrors('totp'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $totp = TOTP::create($account->otp_secret);
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->password = '12345678';
 | 
					 | 
				
			||||||
        $model->totp = $totp->now();
 | 
					 | 
				
			||||||
        $model->validateTotp('totp');
 | 
					 | 
				
			||||||
        $this->assertEmpty($model->getErrors('totp'));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testValidateActivity(): void {
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->status = Account::STATUS_REGISTERED;
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->validateActivity('login');
 | 
					 | 
				
			||||||
        $this->assertSame(['error.account_not_activated'], $model->getErrors('login'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->status = Account::STATUS_BANNED;
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->validateActivity('login');
 | 
					 | 
				
			||||||
        $this->assertSame(['error.account_banned'], $model->getErrors('login'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->status = Account::STATUS_ACTIVE;
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->validateActivity('login');
 | 
					 | 
				
			||||||
        $this->assertEmpty($model->getErrors('login'));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testLogin(): void {
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->id = 1;
 | 
					 | 
				
			||||||
        $account->username = 'erickskrauch';
 | 
					 | 
				
			||||||
        $account->password_hash = '$2y$04$N0q8DaHzlYILCnLYrpZfEeWKEqkPZzbawiS07GbSr/.xbRNweSLU6'; // 12345678
 | 
					 | 
				
			||||||
        $account->password_hash_strategy = Account::PASS_HASH_STRATEGY_YII2;
 | 
					 | 
				
			||||||
        $account->status = Account::STATUS_ACTIVE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->login = 'erickskrauch';
 | 
					 | 
				
			||||||
        $model->password = '12345678';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $this->assertNotNull($model->login(), 'model should login user');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testLoginWithRehashing(): void {
 | 
					 | 
				
			||||||
        /** @var Account $account */
 | 
					 | 
				
			||||||
        $account = $this->tester->grabFixture('accounts', 'user-with-old-password-type');
 | 
					 | 
				
			||||||
        $model = $this->createWithAccount($account);
 | 
					 | 
				
			||||||
        $model->login = $account->username;
 | 
					 | 
				
			||||||
        $model->password = '12345678';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $this->assertNotNull($model->login());
 | 
					 | 
				
			||||||
        $this->assertSame(Account::PASS_HASH_STRATEGY_YII2, $account->password_hash_strategy);
 | 
					 | 
				
			||||||
        $this->assertNotSame('133c00c463cbd3e491c28cb653ce4718', $account->password_hash);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private function createWithAccount(?Account $account): LoginForm {
 | 
					 | 
				
			||||||
        $model = $this->createPartialMock(LoginForm::class, ['getAccount']);
 | 
					 | 
				
			||||||
        $model->method('getAccount')->willReturn($account);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $model;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,37 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
declare(strict_types=1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace api\tests\unit\models\authentication;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use api\components\User\Component;
 | 
					 | 
				
			||||||
use api\models\authentication\LogoutForm;
 | 
					 | 
				
			||||||
use api\tests\unit\TestCase;
 | 
					 | 
				
			||||||
use common\models\AccountSession;
 | 
					 | 
				
			||||||
use Yii;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LogoutFormTest extends TestCase {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testNoActionWhenThereIsNoActiveSession(): void {
 | 
					 | 
				
			||||||
        $userComp = $this->createPartialMock(Component::class, ['getActiveSession']);
 | 
					 | 
				
			||||||
        $userComp->method('getActiveSession')->willReturn(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Yii::$app->set('user', $userComp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $model = new LogoutForm();
 | 
					 | 
				
			||||||
        $this->assertTrue($model->logout());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testActiveSessionShouldBeDeleted(): void {
 | 
					 | 
				
			||||||
        $session = $this->createPartialMock(AccountSession::class, ['delete']);
 | 
					 | 
				
			||||||
        $session->expects($this->once())->method('delete')->willReturn(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $userComp = $this->createPartialMock(Component::class, ['getActiveSession']);
 | 
					 | 
				
			||||||
        $userComp->method('getActiveSession')->willReturn($session);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Yii::$app->set('user', $userComp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $model = new LogoutForm();
 | 
					 | 
				
			||||||
        $model->logout();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -5,10 +5,9 @@ namespace api\tests\unit\modules\accounts\models;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use api\modules\accounts\models\DisableTwoFactorAuthForm;
 | 
					use api\modules\accounts\models\DisableTwoFactorAuthForm;
 | 
				
			||||||
use api\tests\unit\TestCase;
 | 
					use api\tests\unit\TestCase;
 | 
				
			||||||
use common\helpers\Error as E;
 | 
					 | 
				
			||||||
use common\models\Account;
 | 
					use common\models\Account;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DisableTwoFactorAuthFormTest extends TestCase {
 | 
					final class DisableTwoFactorAuthFormTest extends TestCase {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testPerformAction(): void {
 | 
					    public function testPerformAction(): void {
 | 
				
			||||||
        $account = $this->createPartialMock(Account::class, ['save']);
 | 
					        $account = $this->createPartialMock(Account::class, ['save']);
 | 
				
			||||||
@@ -26,18 +25,4 @@ class DisableTwoFactorAuthFormTest extends TestCase {
 | 
				
			|||||||
        $this->assertFalse($account->is_otp_enabled);
 | 
					        $this->assertFalse($account->is_otp_enabled);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testValidateOtpEnabled(): void {
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->is_otp_enabled = false;
 | 
					 | 
				
			||||||
        $model = new DisableTwoFactorAuthForm($account);
 | 
					 | 
				
			||||||
        $model->validateOtpEnabled('account');
 | 
					 | 
				
			||||||
        $this->assertSame([E::OTP_NOT_ENABLED], $model->getErrors('account'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->is_otp_enabled = true;
 | 
					 | 
				
			||||||
        $model = new DisableTwoFactorAuthForm($account);
 | 
					 | 
				
			||||||
        $model->validateOtpEnabled('account');
 | 
					 | 
				
			||||||
        $this->assertEmpty($model->getErrors('account'));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,11 +6,10 @@ namespace api\tests\unit\modules\accounts\models;
 | 
				
			|||||||
use api\components\User\Component;
 | 
					use api\components\User\Component;
 | 
				
			||||||
use api\modules\accounts\models\EnableTwoFactorAuthForm;
 | 
					use api\modules\accounts\models\EnableTwoFactorAuthForm;
 | 
				
			||||||
use api\tests\unit\TestCase;
 | 
					use api\tests\unit\TestCase;
 | 
				
			||||||
use common\helpers\Error as E;
 | 
					 | 
				
			||||||
use common\models\Account;
 | 
					use common\models\Account;
 | 
				
			||||||
use Yii;
 | 
					use Yii;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EnableTwoFactorAuthFormTest extends TestCase {
 | 
					final class EnableTwoFactorAuthFormTest extends TestCase {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testPerformAction(): void {
 | 
					    public function testPerformAction(): void {
 | 
				
			||||||
        $account = $this->createPartialMock(Account::class, ['save']);
 | 
					        $account = $this->createPartialMock(Account::class, ['save']);
 | 
				
			||||||
@@ -30,18 +29,4 @@ class EnableTwoFactorAuthFormTest extends TestCase {
 | 
				
			|||||||
        $this->assertTrue($account->is_otp_enabled);
 | 
					        $this->assertTrue($account->is_otp_enabled);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testValidateOtpDisabled(): void {
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->is_otp_enabled = true;
 | 
					 | 
				
			||||||
        $model = new EnableTwoFactorAuthForm($account);
 | 
					 | 
				
			||||||
        $model->validateOtpDisabled('account');
 | 
					 | 
				
			||||||
        $this->assertSame([E::OTP_ALREADY_ENABLED], $model->getErrors('account'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $account = new Account();
 | 
					 | 
				
			||||||
        $account->is_otp_enabled = false;
 | 
					 | 
				
			||||||
        $model = new EnableTwoFactorAuthForm($account);
 | 
					 | 
				
			||||||
        $model->validateOtpDisabled('account');
 | 
					 | 
				
			||||||
        $this->assertEmpty($model->getErrors('account'));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,116 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
declare(strict_types=1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace api\tests\unit\modules\authserver\models;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use api\modules\authserver\exceptions\ForbiddenOperationException;
 | 
					 | 
				
			||||||
use api\modules\authserver\models\AuthenticationForm;
 | 
					 | 
				
			||||||
use api\tests\unit\TestCase;
 | 
					 | 
				
			||||||
use common\models\Account;
 | 
					 | 
				
			||||||
use common\models\OauthClient;
 | 
					 | 
				
			||||||
use common\models\OauthSession;
 | 
					 | 
				
			||||||
use common\tests\fixtures\AccountFixture;
 | 
					 | 
				
			||||||
use common\tests\fixtures\OauthClientFixture;
 | 
					 | 
				
			||||||
use OTPHP\TOTP;
 | 
					 | 
				
			||||||
use function Ramsey\Uuid\v4 as uuid4;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AuthenticationFormTest extends TestCase {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function _fixtures(): array {
 | 
					 | 
				
			||||||
        return [
 | 
					 | 
				
			||||||
            'accounts' => AccountFixture::class,
 | 
					 | 
				
			||||||
            'oauthClients' => OauthClientFixture::class,
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testAuthenticateByValidCredentials(): void {
 | 
					 | 
				
			||||||
        $authForm = new AuthenticationForm();
 | 
					 | 
				
			||||||
        $authForm->username = 'admin';
 | 
					 | 
				
			||||||
        $authForm->password = 'password_0';
 | 
					 | 
				
			||||||
        $authForm->clientToken = uuid4();
 | 
					 | 
				
			||||||
        $result = $authForm->authenticate()->getResponseData();
 | 
					 | 
				
			||||||
        $this->assertMatchesRegularExpression('/^[\w=-]+\.[\w=-]+\.[\w=-]+$/', $result['accessToken']);
 | 
					 | 
				
			||||||
        $this->assertSame($authForm->clientToken, $result['clientToken']);
 | 
					 | 
				
			||||||
        $this->assertSame('df936908b2e1544d96f82977ec213022', $result['selectedProfile']['id']);
 | 
					 | 
				
			||||||
        $this->assertSame('Admin', $result['selectedProfile']['name']);
 | 
					 | 
				
			||||||
        $this->assertTrue(OauthSession::find()->andWhere([
 | 
					 | 
				
			||||||
            'account_id' => 1,
 | 
					 | 
				
			||||||
            'client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER,
 | 
					 | 
				
			||||||
        ])->exists());
 | 
					 | 
				
			||||||
        $this->assertArrayNotHasKey('user', $result);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $authForm->requestUser = true;
 | 
					 | 
				
			||||||
        $result = $authForm->authenticate()->getResponseData();
 | 
					 | 
				
			||||||
        $this->assertSame([
 | 
					 | 
				
			||||||
            'id' => 'df936908b2e1544d96f82977ec213022',
 | 
					 | 
				
			||||||
            'username' => 'Admin',
 | 
					 | 
				
			||||||
            'properties' => [
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                    'name' => 'preferredLanguage',
 | 
					 | 
				
			||||||
                    'value' => 'en',
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        ], $result['user']);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function testAuthenticateByValidCredentialsWith2FA(): void {
 | 
					 | 
				
			||||||
        $authForm = new AuthenticationForm();
 | 
					 | 
				
			||||||
        $authForm->username = 'otp@gmail.com';
 | 
					 | 
				
			||||||
        $authForm->password = 'password_0:' . TOTP::create('BBBB')->now();
 | 
					 | 
				
			||||||
        $authForm->clientToken = uuid4();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Just ensure that there is no exception
 | 
					 | 
				
			||||||
        $this->expectNotToPerformAssertions();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $authForm->authenticate();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * This is a special case which ensures that if the user has a password that looks like
 | 
					 | 
				
			||||||
     * a two-factor code passed in the password field, than he can still log in into his account
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function testAuthenticateEdgyCaseFor2FA(): void {
 | 
					 | 
				
			||||||
        /** @var Account $account */
 | 
					 | 
				
			||||||
        $account = Account::findOne(['email' => 'admin@ely.by']);
 | 
					 | 
				
			||||||
        $account->setPassword('password_0:123456');
 | 
					 | 
				
			||||||
        $account->save();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $authForm = new AuthenticationForm();
 | 
					 | 
				
			||||||
        $authForm->username = 'admin@ely.by';
 | 
					 | 
				
			||||||
        $authForm->password = 'password_0:123456';
 | 
					 | 
				
			||||||
        $authForm->clientToken = uuid4();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Just ensure that there is no exception
 | 
					 | 
				
			||||||
        $this->expectNotToPerformAssertions();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $authForm->authenticate();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @dataProvider getInvalidCredentialsCases
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public function testAuthenticateByWrongCredentials(
 | 
					 | 
				
			||||||
        string $expectedExceptionMessage,
 | 
					 | 
				
			||||||
        string $login,
 | 
					 | 
				
			||||||
        string $password,
 | 
					 | 
				
			||||||
        string $totp = null,
 | 
					 | 
				
			||||||
    ): void {
 | 
					 | 
				
			||||||
        $this->expectException(ForbiddenOperationException::class);
 | 
					 | 
				
			||||||
        $this->expectExceptionMessage($expectedExceptionMessage);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $authForm = new AuthenticationForm();
 | 
					 | 
				
			||||||
        $authForm->username = $login;
 | 
					 | 
				
			||||||
        $authForm->password = $password . ($totp ? ":{$totp}" : '');
 | 
					 | 
				
			||||||
        $authForm->clientToken = uuid4();
 | 
					 | 
				
			||||||
        $authForm->authenticate();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function getInvalidCredentialsCases(): iterable {
 | 
					 | 
				
			||||||
        yield ['Invalid credentials. Invalid nickname or password.', 'wrong-username', 'wrong-password'];
 | 
					 | 
				
			||||||
        yield ['Invalid credentials. Invalid email or password.', 'wrong-email@ely.by', 'wrong-password'];
 | 
					 | 
				
			||||||
        yield ['This account has been suspended.', 'Banned', 'password_0'];
 | 
					 | 
				
			||||||
        yield ['Account protected with two factor auth.', 'AccountWithEnabledOtp', 'password_0'];
 | 
					 | 
				
			||||||
        yield ['Invalid credentials. Invalid nickname or password.', 'AccountWithEnabledOtp', 'password_0', '123456'];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,15 +1,17 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api\tests\unit\validators;
 | 
					namespace api\tests\unit\validators;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use api\tests\unit\TestCase;
 | 
					use api\tests\unit\TestCase;
 | 
				
			||||||
use api\validators\TotpValidator;
 | 
					use api\validators\TotpValidator;
 | 
				
			||||||
 | 
					use Carbon\CarbonImmutable;
 | 
				
			||||||
use common\helpers\Error as E;
 | 
					use common\helpers\Error as E;
 | 
				
			||||||
use common\models\Account;
 | 
					use common\models\Account;
 | 
				
			||||||
use common\tests\_support\ProtectedCaller;
 | 
					use Lcobucci\Clock\FrozenClock;
 | 
				
			||||||
use OTPHP\TOTP;
 | 
					use OTPHP\TOTP;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TotpValidatorTest extends TestCase {
 | 
					final class TotpValidatorTest extends TestCase {
 | 
				
			||||||
    use ProtectedCaller;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function testValidateValue(): void {
 | 
					    public function testValidateValue(): void {
 | 
				
			||||||
        $account = new Account();
 | 
					        $account = new Account();
 | 
				
			||||||
@@ -18,32 +20,25 @@ class TotpValidatorTest extends TestCase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $validator = new TotpValidator(['account' => $account]);
 | 
					        $validator = new TotpValidator(['account' => $account]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $result = $this->callProtected($validator, 'validateValue', 123456);
 | 
					        $this->assertFalse($validator->validate(123456, $error));
 | 
				
			||||||
        $this->assertSame([E::TOTP_INCORRECT, []], $result);
 | 
					        $this->assertSame(E::TOTP_INCORRECT, $error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $result = $this->callProtected($validator, 'validateValue', $controlTotp->now());
 | 
					        $error = null;
 | 
				
			||||||
        $this->assertNull($result);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $result = $this->callProtected($validator, 'validateValue', $controlTotp->at(time() - 31));
 | 
					        $this->assertTrue($validator->validate($controlTotp->now(), $error));
 | 
				
			||||||
        $this->assertNull($result);
 | 
					        $this->assertNull($error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $at = time() - 400;
 | 
					        $error = null;
 | 
				
			||||||
        $validator->timestamp = $at;
 | 
					 | 
				
			||||||
        $result = $this->callProtected($validator, 'validateValue', $controlTotp->now());
 | 
					 | 
				
			||||||
        $this->assertSame([E::TOTP_INCORRECT, []], $result);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $result = $this->callProtected($validator, 'validateValue', $controlTotp->at($at));
 | 
					        // @phpstan-ignore argument.type
 | 
				
			||||||
        $this->assertNull($result);
 | 
					        $this->assertTrue($validator->validate($controlTotp->at(time() - 31), $error));
 | 
				
			||||||
 | 
					        $this->assertNull($error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $at = fn(): ?int => null;
 | 
					        $error = null;
 | 
				
			||||||
        $validator->timestamp = $at;
 | 
					 | 
				
			||||||
        $result = $this->callProtected($validator, 'validateValue', $controlTotp->now());
 | 
					 | 
				
			||||||
        $this->assertNull($result);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $at = fn(): int => time() - 700;
 | 
					        $validator->setClock(new FrozenClock(CarbonImmutable::now()->subSeconds(400)));
 | 
				
			||||||
        $validator->timestamp = $at;
 | 
					        $this->assertFalse($validator->validate($controlTotp->now(), $error));
 | 
				
			||||||
        $result = $this->callProtected($validator, 'validateValue', $controlTotp->at($at()));
 | 
					        $this->assertSame(E::TOTP_INCORRECT, $error);
 | 
				
			||||||
        $this->assertNull($result);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,49 +1,52 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace api\validators;
 | 
					namespace api\validators;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Carbon\FactoryImmutable;
 | 
				
			||||||
use common\helpers\Error as E;
 | 
					use common\helpers\Error as E;
 | 
				
			||||||
use common\models\Account;
 | 
					use common\models\Account;
 | 
				
			||||||
use OTPHP\TOTP;
 | 
					use OTPHP\TOTP;
 | 
				
			||||||
 | 
					use Psr\Clock\ClockInterface;
 | 
				
			||||||
use RangeException;
 | 
					use RangeException;
 | 
				
			||||||
use Yii;
 | 
					 | 
				
			||||||
use yii\base\InvalidConfigException;
 | 
					use yii\base\InvalidConfigException;
 | 
				
			||||||
use yii\validators\Validator;
 | 
					use yii\validators\Validator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TotpValidator extends Validator {
 | 
					final class TotpValidator extends Validator {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public ?Account $account = null;
 | 
					    public ?Account $account = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @var int|callable|null Allows you to set the exact time against which the validation will be performed.
 | 
					 | 
				
			||||||
     * It may be the unix time or a function returning a unix time.
 | 
					 | 
				
			||||||
     * If not specified, the current time will be used.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public mixed $timestamp = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public $skipOnEmpty = false;
 | 
					    public $skipOnEmpty = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private ClockInterface $clock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @throws InvalidConfigException
 | 
					     * @throws InvalidConfigException
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public function init(): void {
 | 
					    public function init(): void {
 | 
				
			||||||
        parent::init();
 | 
					        parent::init();
 | 
				
			||||||
        if ($this->account === null) {
 | 
					 | 
				
			||||||
            $this->account = Yii::$app->user->identity;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!$this->account instanceof Account) {
 | 
					        if (!$this->account instanceof Account) {
 | 
				
			||||||
            throw new InvalidConfigException('account should be instance of ' . Account::class);
 | 
					            throw new InvalidConfigException('This validator must be instantiated with the account param');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (empty($this->account->otp_secret)) {
 | 
					        if (empty($this->account->otp_secret)) {
 | 
				
			||||||
            throw new InvalidConfigException('account should have not empty otp_secret');
 | 
					            throw new InvalidConfigException('account should have not empty otp_secret');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->clock = FactoryImmutable::getDefaultInstance();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function setClock(ClockInterface $clock): void {
 | 
				
			||||||
 | 
					        $this->clock = $clock;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected function validateValue($value): ?array {
 | 
					    protected function validateValue($value): ?array {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 | 
					            // @phpstan-ignore argument.type (it is non empty, its checked in the init method)
 | 
				
			||||||
            $totp = TOTP::create($this->account->otp_secret);
 | 
					            $totp = TOTP::create($this->account->otp_secret);
 | 
				
			||||||
            if (!$totp->verify((string)$value, $this->getTimestamp(), $totp->getPeriod() - 1)) {
 | 
					            // @phpstan-ignore argument.type,argument.type,argument.type (all types are fine, they're just not declared well)
 | 
				
			||||||
 | 
					            if (!$totp->verify((string)$value, $this->clock->now()->getTimestamp(), $totp->getPeriod() - 1)) {
 | 
				
			||||||
                return [E::TOTP_INCORRECT, []];
 | 
					                return [E::TOTP_INCORRECT, []];
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (RangeException) {
 | 
					        } catch (RangeException) {
 | 
				
			||||||
@@ -53,17 +56,4 @@ class TotpValidator extends Validator {
 | 
				
			|||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function getTimestamp(): ?int {
 | 
					 | 
				
			||||||
        $timestamp = $this->timestamp;
 | 
					 | 
				
			||||||
        if (is_callable($timestamp)) {
 | 
					 | 
				
			||||||
            $timestamp = call_user_func($this->timestamp);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ($timestamp === null) {
 | 
					 | 
				
			||||||
            return null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return (int)$timestamp;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Entities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use common\models\Account;
 | 
				
			||||||
 | 
					use common\models\AccountSession;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final readonly class AuthenticationResult {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        public Account $account,
 | 
				
			||||||
 | 
					        public ?AccountSession $session = null,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								common/components/Authentication/Entities/Credentials.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								common/components/Authentication/Entities/Credentials.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Entities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final readonly class Credentials {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        public string $login,
 | 
				
			||||||
 | 
					        public string $password,
 | 
				
			||||||
 | 
					        public ?string $totp = null,
 | 
				
			||||||
 | 
					        public bool $rememberMe = false,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use common\models\Account;
 | 
				
			||||||
 | 
					use Exception;
 | 
				
			||||||
 | 
					use Throwable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class AccountBannedException extends Exception implements AuthenticationException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        public readonly Account $account,
 | 
				
			||||||
 | 
					        ?Throwable $previous = null,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        parent::__construct('The account has been banned', previous: $previous);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use common\models\Account;
 | 
				
			||||||
 | 
					use Exception;
 | 
				
			||||||
 | 
					use Throwable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class AccountNotActivatedException extends Exception implements AuthenticationException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        public readonly Account $account,
 | 
				
			||||||
 | 
					        ?Throwable $previous = null,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        parent::__construct('The account has not been activated yet', previous: $previous);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Throwable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AuthenticationException extends Throwable {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Exception;
 | 
				
			||||||
 | 
					use Throwable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class InvalidPasswordException extends Exception implements AuthenticationException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(?Throwable $previous = null) {
 | 
				
			||||||
 | 
					        parent::__construct("The entered password doesn't match the account's password", previous: $previous);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Exception;
 | 
				
			||||||
 | 
					use Throwable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class InvalidTotpException extends Exception implements AuthenticationException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(?Throwable $previous = null) {
 | 
				
			||||||
 | 
					        parent::__construct('Incorrect TOTP value', previous: $previous);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Exception;
 | 
				
			||||||
 | 
					use Throwable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class TotpRequiredException extends Exception implements AuthenticationException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(?Throwable $previous = null) {
 | 
				
			||||||
 | 
					        parent::__construct('Two-factor authentication is enabled for the account and you need to pass the TOTP', previous: $previous);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication\Exceptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Exception;
 | 
				
			||||||
 | 
					use Throwable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class UnknownLoginException extends Exception implements AuthenticationException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function __construct(?Throwable $previous = null) {
 | 
				
			||||||
 | 
					        parent::__construct('The account with the specified login does not exist', previous: $previous);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										67
									
								
								common/components/Authentication/LoginService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								common/components/Authentication/LoginService.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use api\validators\TotpValidator;
 | 
				
			||||||
 | 
					use common\components\Authentication\Entities\AuthenticationResult;
 | 
				
			||||||
 | 
					use common\components\Authentication\Entities\Credentials;
 | 
				
			||||||
 | 
					use common\models\Account;
 | 
				
			||||||
 | 
					use common\models\AccountSession;
 | 
				
			||||||
 | 
					use Webmozart\Assert\Assert;
 | 
				
			||||||
 | 
					use Yii;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class LoginService implements LoginServiceInterface {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function loginByCredentials(Credentials $credentials): AuthenticationResult {
 | 
				
			||||||
 | 
					        /** @var Account|null $account */
 | 
				
			||||||
 | 
					        $account = Account::find()->andWhereLogin($credentials->login)->one();
 | 
				
			||||||
 | 
					        if ($account === null) {
 | 
				
			||||||
 | 
					            throw new Exceptions\UnknownLoginException();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!$account->validatePassword($credentials->password)) {
 | 
				
			||||||
 | 
					            throw new Exceptions\InvalidPasswordException();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($account->is_otp_enabled) {
 | 
				
			||||||
 | 
					            if (empty($credentials->totp)) {
 | 
				
			||||||
 | 
					                throw new Exceptions\TotpRequiredException();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $validator = new TotpValidator(['account' => $account]);
 | 
				
			||||||
 | 
					            if (!$validator->validate($credentials->totp)) {
 | 
				
			||||||
 | 
					                throw new Exceptions\InvalidTotpException();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($account->status === Account::STATUS_BANNED) {
 | 
				
			||||||
 | 
					            throw new Exceptions\AccountBannedException($account);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($account->status === Account::STATUS_REGISTERED) {
 | 
				
			||||||
 | 
					            throw new Exceptions\AccountNotActivatedException($account);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($account->password_hash_strategy !== Account::PASS_HASH_STRATEGY_YII2) {
 | 
				
			||||||
 | 
					            $account->setPassword($credentials->password);
 | 
				
			||||||
 | 
					            Assert::true($account->save(), 'Unable to upgrade user\'s password');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $session = null;
 | 
				
			||||||
 | 
					        if ($credentials->rememberMe) {
 | 
				
			||||||
 | 
					            $session = new AccountSession();
 | 
				
			||||||
 | 
					            $session->account_id = $account->id;
 | 
				
			||||||
 | 
					            $session->setIp(Yii::$app->request->userIP);
 | 
				
			||||||
 | 
					            $session->generateRefreshToken();
 | 
				
			||||||
 | 
					            Assert::true($session->save(), 'Cannot save account session model');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return new AuthenticationResult($account, $session);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function logout(AccountSession $session): void {
 | 
				
			||||||
 | 
					        $session->delete();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								common/components/Authentication/LoginServiceInterface.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								common/components/Authentication/LoginServiceInterface.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					declare(strict_types=1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace common\components\Authentication;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use common\components\Authentication\Entities\AuthenticationResult;
 | 
				
			||||||
 | 
					use common\components\Authentication\Entities\Credentials;
 | 
				
			||||||
 | 
					use common\models\AccountSession;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface LoginServiceInterface {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @throws \common\components\Authentication\Exceptions\AuthenticationException
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function loginByCredentials(Credentials $credentials): AuthenticationResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function logout(AccountSession $session): void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -27,6 +27,7 @@ return [
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            League\OAuth2\Server\AuthorizationServer::class => common\components\OAuth2\AuthorizationServerFactory::build(...),
 | 
					            League\OAuth2\Server\AuthorizationServer::class => common\components\OAuth2\AuthorizationServerFactory::build(...),
 | 
				
			||||||
 | 
					            common\components\Authentication\LoginServiceInterface::class => common\components\Authentication\LoginService::class,
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    'components' => [
 | 
					    'components' => [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,6 +77,10 @@ class Account extends ActiveRecord {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function validatePassword(string $password, int $passwordHashStrategy = null): bool {
 | 
					    public function validatePassword(string $password, int $passwordHashStrategy = null): bool {
 | 
				
			||||||
 | 
					        if (empty($password)) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($passwordHashStrategy === null) {
 | 
					        if ($passwordHashStrategy === null) {
 | 
				
			||||||
            $passwordHashStrategy = $this->password_hash_strategy;
 | 
					            $passwordHashStrategy = $this->password_hash_strategy;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ namespace common\models;
 | 
				
			|||||||
use yii\db\ActiveQuery;
 | 
					use yii\db\ActiveQuery;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @see Account
 | 
					 * @extends \yii\db\ActiveQuery<\common\models\Account>
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class AccountQuery extends ActiveQuery {
 | 
					class AccountQuery extends ActiveQuery {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -330,36 +330,6 @@ parameters:
 | 
				
			|||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
			path: api/modules/accounts/models/DeleteAccountForm.php
 | 
								path: api/modules/accounts/models/DeleteAccountForm.php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Method api\\\\modules\\\\accounts\\\\models\\\\DisableTwoFactorAuthForm\\:\\:validateOtpEnabled\\(\\) has parameter \\$attribute with no type specified\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/modules/accounts/models/DisableTwoFactorAuthForm.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Property api\\\\modules\\\\accounts\\\\models\\\\DisableTwoFactorAuthForm\\:\\:\\$password has no type specified\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/modules/accounts/models/DisableTwoFactorAuthForm.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Property api\\\\modules\\\\accounts\\\\models\\\\DisableTwoFactorAuthForm\\:\\:\\$totp has no type specified\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/modules/accounts/models/DisableTwoFactorAuthForm.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Method api\\\\modules\\\\accounts\\\\models\\\\EnableTwoFactorAuthForm\\:\\:validateOtpDisabled\\(\\) has parameter \\$attribute with no type specified\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/modules/accounts/models/EnableTwoFactorAuthForm.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Property api\\\\modules\\\\accounts\\\\models\\\\EnableTwoFactorAuthForm\\:\\:\\$password has no type specified\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/modules/accounts/models/EnableTwoFactorAuthForm.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Property api\\\\modules\\\\accounts\\\\models\\\\EnableTwoFactorAuthForm\\:\\:\\$totp has no type specified\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/modules/accounts/models/EnableTwoFactorAuthForm.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
							-
 | 
				
			||||||
			message: "#^Property api\\\\modules\\\\accounts\\\\models\\\\SendEmailVerificationForm\\:\\:\\$password has no type specified\\.$#"
 | 
								message: "#^Property api\\\\modules\\\\accounts\\\\models\\\\SendEmailVerificationForm\\:\\:\\$password has no type specified\\.$#"
 | 
				
			||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
@@ -415,11 +385,6 @@ parameters:
 | 
				
			|||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
			path: api/modules/authserver/controllers/AuthenticationController.php
 | 
								path: api/modules/authserver/controllers/AuthenticationController.php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Method api\\\\modules\\\\authserver\\\\models\\\\AuthenticateData\\:\\:getResponseData\\(\\) return type has no value type specified in iterable type array\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/modules/authserver/models/AuthenticateData.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
							-
 | 
				
			||||||
			message: "#^Method api\\\\modules\\\\authserver\\\\validators\\\\AccessTokenValidator\\:\\:validateValue\\(\\) return type has no value type specified in iterable type array\\.$#"
 | 
								message: "#^Method api\\\\modules\\\\authserver\\\\validators\\\\AccessTokenValidator\\:\\:validateValue\\(\\) return type has no value type specified in iterable type array\\.$#"
 | 
				
			||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
@@ -860,11 +825,6 @@ parameters:
 | 
				
			|||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
			path: api/tests/functional/authserver/RefreshCest.php
 | 
								path: api/tests/functional/authserver/RefreshCest.php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Method api\\\\tests\\\\functional\\\\authserver\\\\SignoutCest\\:\\:signout\\(\\) has parameter \\$example with no value type specified in iterable type Codeception\\\\Example\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/tests/functional/authserver/SignoutCest.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
							-
 | 
				
			||||||
			message: "#^Method api\\\\tests\\\\functional\\\\authserver\\\\UsernamesToUuidsCest\\:\\:bulkProfilesEndpoints\\(\\) return type has no value type specified in iterable type array\\.$#"
 | 
								message: "#^Method api\\\\tests\\\\functional\\\\authserver\\\\UsernamesToUuidsCest\\:\\:bulkProfilesEndpoints\\(\\) return type has no value type specified in iterable type array\\.$#"
 | 
				
			||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
@@ -1045,11 +1005,6 @@ parameters:
 | 
				
			|||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
			path: api/tests/unit/modules/accounts/models/ChangeUsernameFormTest.php
 | 
								path: api/tests/unit/modules/accounts/models/ChangeUsernameFormTest.php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Method api\\\\tests\\\\unit\\\\modules\\\\authserver\\\\models\\\\AuthenticationFormTest\\:\\:getInvalidCredentialsCases\\(\\) return type has no value type specified in iterable type iterable\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/tests/unit/modules/authserver/models/AuthenticationFormTest.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
							-
 | 
				
			||||||
			message: "#^Method api\\\\tests\\\\unit\\\\modules\\\\authserver\\\\validators\\\\RequiredValidatorTest\\:\\:callProtected\\(\\) has no return type specified\\.$#"
 | 
								message: "#^Method api\\\\tests\\\\unit\\\\modules\\\\authserver\\\\validators\\\\RequiredValidatorTest\\:\\:callProtected\\(\\) has no return type specified\\.$#"
 | 
				
			||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
@@ -1100,31 +1055,6 @@ parameters:
 | 
				
			|||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
			path: api/tests/unit/validators/PasswordRequiredValidatorTest.php
 | 
								path: api/tests/unit/validators/PasswordRequiredValidatorTest.php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Method api\\\\tests\\\\unit\\\\validators\\\\TotpValidatorTest\\:\\:callProtected\\(\\) has no return type specified\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/tests/unit/validators/TotpValidatorTest.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Method api\\\\tests\\\\unit\\\\validators\\\\TotpValidatorTest\\:\\:callProtected\\(\\) has parameter \\$args with no type specified\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/tests/unit/validators/TotpValidatorTest.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Parameter \\#1 \\$input of method OTPHP\\\\TOTP\\:\\:at\\(\\) expects int\\<0, max\\>, int\\<\\-30, max\\> given\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/tests/unit/validators/TotpValidatorTest.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Parameter \\#1 \\$input of method OTPHP\\\\TOTP\\:\\:at\\(\\) expects int\\<0, max\\>, int\\<\\-399, max\\> given\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/tests/unit/validators/TotpValidatorTest.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Parameter \\#1 \\$input of method OTPHP\\\\TOTP\\:\\:at\\(\\) expects int\\<0, max\\>, int\\<\\-699, max\\> given\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/tests/unit/validators/TotpValidatorTest.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
							-
 | 
				
			||||||
			message: "#^Property api\\\\validators\\\\EmailActivationKeyValidator\\:\\:\\$expired has no type specified\\.$#"
 | 
								message: "#^Property api\\\\validators\\\\EmailActivationKeyValidator\\:\\:\\$expired has no type specified\\.$#"
 | 
				
			||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
@@ -1150,31 +1080,6 @@ parameters:
 | 
				
			|||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
			path: api/validators/PasswordRequiredValidator.php
 | 
								path: api/validators/PasswordRequiredValidator.php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Parameter \\#1 \\$callback of function call_user_func expects callable\\(\\)\\: mixed, \\(callable\\(\\)\\: mixed\\)\\|int\\|null given\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/validators/TotpValidator.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Parameter \\#1 \\$otp of method OTPHP\\\\TOTP\\:\\:verify\\(\\) expects non\\-empty\\-string, string given\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/validators/TotpValidator.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Parameter \\#1 \\$secret of static method OTPHP\\\\TOTP\\:\\:create\\(\\) expects non\\-empty\\-string\\|null, string\\|null given\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/validators/TotpValidator.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Parameter \\#2 \\$timestamp of method OTPHP\\\\TOTP\\:\\:verify\\(\\) expects int\\<0, max\\>\\|null, int\\|null given\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/validators/TotpValidator.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Parameter \\#3 \\$leeway of method OTPHP\\\\TOTP\\:\\:verify\\(\\) expects int\\<0, max\\>\\|null, int given\\.$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: api/validators/TotpValidator.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
							-
 | 
				
			||||||
			message: "#^Method common\\\\components\\\\EmailsRenderer\\\\Request\\\\TemplateRequest\\:\\:__construct\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
 | 
								message: "#^Method common\\\\components\\\\EmailsRenderer\\\\Request\\\\TemplateRequest\\:\\:__construct\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
 | 
				
			||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
@@ -1285,11 +1190,6 @@ parameters:
 | 
				
			|||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
			path: common/models/Account.php
 | 
								path: common/models/Account.php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		-
 | 
					 | 
				
			||||||
			message: "#^Class common\\\\models\\\\AccountQuery extends generic class yii\\\\db\\\\ActiveQuery but does not specify its types\\: T$#"
 | 
					 | 
				
			||||||
			count: 1
 | 
					 | 
				
			||||||
			path: common/models/AccountQuery.php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		-
 | 
							-
 | 
				
			||||||
			message: "#^Method common\\\\models\\\\AccountSession\\:\\:getAccount\\(\\) return type with generic class yii\\\\db\\\\ActiveQuery does not specify its types\\: T$#"
 | 
								message: "#^Method common\\\\models\\\\AccountSession\\:\\:getAccount\\(\\) return type with generic class yii\\\\db\\\\ActiveQuery does not specify its types\\: T$#"
 | 
				
			||||||
			count: 1
 | 
								count: 1
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user