From be4697e6ebc33d0edc21d4e2dcd96aada232cb59 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 17 Jan 2025 21:37:35 +0100 Subject: [PATCH] Extract login logics into a separate component. Not quite clean result but enough for upcoming tasks --- api/components/Tokens/TokensFactory.php | 1 - api/controllers/AuthenticationController.php | 78 +++++++++-- api/eventListeners/LogMetricsToStatsd.php | 5 +- .../authentication/AuthenticationResult.php | 1 + api/models/authentication/LoginForm.php | 98 +------------- api/models/authentication/LogoutForm.php | 23 ---- api/models/base/ApiForm.php | 2 +- .../models/DisableTwoFactorAuthForm.php | 10 +- .../models/EnableTwoFactorAuthForm.php | 10 +- .../controllers/AuthenticationController.php | 11 +- .../authserver/models/AuthenticateData.php | 22 ++++ .../authserver/models/AuthenticationForm.php | 113 +++++++--------- api/modules/authserver/models/SignoutForm.php | 38 +----- api/tests/_pages/AuthenticationRoute.php | 10 +- api/tests/functional/LoginCest.php | 2 +- .../authserver/AuthorizationCest.php | 4 +- .../functional/authserver/SignoutCest.php | 16 +-- .../models/authentication/LoginFormTest.php | 122 ------------------ .../models/authentication/LogoutFormTest.php | 37 ------ .../models/DisableTwoFactorAuthFormTest.php | 17 +-- .../models/EnableTwoFactorAuthFormTest.php | 17 +-- .../models/AuthenticationFormTest.php | 116 ----------------- .../unit/validators/TotpValidatorTest.php | 41 +++--- api/validators/TotpValidator.php | 44 +++---- .../Entities/AuthenticationResult.php | 17 +++ .../Authentication/Entities/Credentials.php | 16 +++ .../Exceptions/AccountBannedException.php | 19 +++ .../AccountNotActivatedException.php | 19 +++ .../Exceptions/AuthenticationException.php | 10 ++ .../Exceptions/InvalidPasswordException.php | 15 +++ .../Exceptions/InvalidTotpException.php | 15 +++ .../Exceptions/TotpRequiredException.php | 15 +++ .../Exceptions/UnknownLoginException.php | 15 +++ .../Authentication/LoginService.php | 67 ++++++++++ .../Authentication/LoginServiceInterface.php | 19 +++ common/config/config.php | 1 + common/models/Account.php | 4 + common/models/AccountQuery.php | 2 +- phpstan-baseline.neon | 100 -------------- 39 files changed, 443 insertions(+), 729 deletions(-) delete mode 100644 api/models/authentication/LogoutForm.php delete mode 100644 api/tests/unit/models/authentication/LoginFormTest.php delete mode 100644 api/tests/unit/models/authentication/LogoutFormTest.php delete mode 100644 api/tests/unit/modules/authserver/models/AuthenticationFormTest.php create mode 100644 common/components/Authentication/Entities/AuthenticationResult.php create mode 100644 common/components/Authentication/Entities/Credentials.php create mode 100644 common/components/Authentication/Exceptions/AccountBannedException.php create mode 100644 common/components/Authentication/Exceptions/AccountNotActivatedException.php create mode 100644 common/components/Authentication/Exceptions/AuthenticationException.php create mode 100644 common/components/Authentication/Exceptions/InvalidPasswordException.php create mode 100644 common/components/Authentication/Exceptions/InvalidTotpException.php create mode 100644 common/components/Authentication/Exceptions/TotpRequiredException.php create mode 100644 common/components/Authentication/Exceptions/UnknownLoginException.php create mode 100644 common/components/Authentication/LoginService.php create mode 100644 common/components/Authentication/LoginServiceInterface.php diff --git a/api/components/Tokens/TokensFactory.php b/api/components/Tokens/TokensFactory.php index c7f5244..b4f5ba9 100644 --- a/api/components/Tokens/TokensFactory.php +++ b/api/components/Tokens/TokensFactory.php @@ -9,7 +9,6 @@ use Carbon\Carbon; use common\models\Account; use common\models\AccountSession; use DateTime; -use Lcobucci\JWT\Token; use Lcobucci\JWT\UnencryptedToken; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; diff --git a/api/controllers/AuthenticationController.php b/api/controllers/AuthenticationController.php index bbd08a4..f96dcb9 100644 --- a/api/controllers/AuthenticationController.php +++ b/api/controllers/AuthenticationController.php @@ -1,18 +1,35 @@ ['POST'], 'logout' => ['POST'], @@ -48,30 +65,63 @@ class AuthenticationController extends Controller { ]; } - public function actionLogin(): array { - $model = new LoginForm(); - $model->load(Yii::$app->request->post()); - if (($result = $model->login()) === null) { + public function actionLogin(Request $request): array { + $form = new LoginForm(); + $form->load($request->post()); + 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 = [ '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) { - $data['data']['email'] = $model->getAccount()->email; + if ($e instanceof Exceptions\AccountNotActivatedException) { + $data['data']['email'] = $e->account->email; } return $data; } - return array_merge([ + $token = Yii::$app->tokensFactory->createForWebAccount($loginResult->account, $loginResult->session); + $data = [ '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 { - $form = new LogoutForm(); - $form->logout(); + $session = Yii::$app->user->getActiveSession(); + if ($session) { + $this->loginService->logout($session); + } return [ 'success' => true, diff --git a/api/eventListeners/LogMetricsToStatsd.php b/api/eventListeners/LogMetricsToStatsd.php index aaaca16..87a6597 100644 --- a/api/eventListeners/LogMetricsToStatsd.php +++ b/api/eventListeners/LogMetricsToStatsd.php @@ -6,7 +6,6 @@ namespace api\eventListeners; use api\controllers\AuthenticationController; use api\controllers\SignupController; use api\modules\accounts\actions; -use Closure; use Yii; use yii\base\ActionEvent; use yii\base\BootstrapInterface; @@ -16,8 +15,8 @@ use yii\base\Event; final class LogMetricsToStatsd implements BootstrapInterface { public function bootstrap($app): void { - Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, Closure::fromCallable([$this, 'beforeAction'])); - Event::on(Controller::class, Controller::EVENT_AFTER_ACTION, Closure::fromCallable([$this, 'afterAction'])); + Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, $this->beforeAction(...)); + Event::on(Controller::class, Controller::EVENT_AFTER_ACTION, $this->afterAction(...)); } private function beforeAction(ActionEvent $event): void { diff --git a/api/models/authentication/AuthenticationResult.php b/api/models/authentication/AuthenticationResult.php index 5d1b750..ff7cad2 100644 --- a/api/models/authentication/AuthenticationResult.php +++ b/api/models/authentication/AuthenticationResult.php @@ -6,6 +6,7 @@ namespace api\models\authentication; use DateTimeImmutable; use Lcobucci\JWT\UnencryptedToken; +// TODO: remove this class final readonly class AuthenticationResult { public function __construct( diff --git a/api/models/authentication/LoginForm.php b/api/models/authentication/LoginForm.php index 2ab1ec4..76a4731 100644 --- a/api/models/authentication/LoginForm.php +++ b/api/models/authentication/LoginForm.php @@ -4,14 +4,9 @@ declare(strict_types=1); namespace api\models\authentication; use api\models\base\ApiForm; -use api\validators\TotpValidator; 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; @@ -24,97 +19,10 @@ class LoginForm extends ApiForm { public function rules(): array { return [ ['login', 'required', 'message' => E::LOGIN_REQUIRED], - ['login', 'validateLogin'], - - ['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'], - + ['password', 'required', 'isEmpty' => fn($value) => $value === null, 'message' => E::PASSWORD_REQUIRED], + ['totp', 'string'], ['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); - } - } diff --git a/api/models/authentication/LogoutForm.php b/api/models/authentication/LogoutForm.php deleted file mode 100644 index 5d9924f..0000000 --- a/api/models/authentication/LogoutForm.php +++ /dev/null @@ -1,23 +0,0 @@ -user; - $session = $component->getActiveSession(); - if ($session === null) { - return true; - } - - $session->delete(); - - return true; - } - -} diff --git a/api/models/base/ApiForm.php b/api/models/base/ApiForm.php index 70e0f05..e92513b 100644 --- a/api/models/base/ApiForm.php +++ b/api/models/base/ApiForm.php @@ -5,7 +5,7 @@ namespace api\models\base; use yii\base\Model; -class ApiForm extends Model { +abstract class ApiForm extends Model { public function formName(): string { return ''; diff --git a/api/modules/accounts/models/DisableTwoFactorAuthForm.php b/api/modules/accounts/models/DisableTwoFactorAuthForm.php index 5aa5c7d..1ab2232 100644 --- a/api/modules/accounts/models/DisableTwoFactorAuthForm.php +++ b/api/modules/accounts/models/DisableTwoFactorAuthForm.php @@ -1,4 +1,6 @@ validateOtpEnabled(...)], ['totp', 'required', 'message' => E::TOTP_REQUIRED], ['totp', TotpValidator::class, 'account' => $this->getAccount()], ['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()], @@ -34,7 +36,7 @@ class DisableTwoFactorAuthForm extends AccountActionForm { return true; } - public function validateOtpEnabled($attribute): void { + private function validateOtpEnabled(string $attribute): void { if (!$this->getAccount()->is_otp_enabled) { $this->addError($attribute, E::OTP_NOT_ENABLED); } diff --git a/api/modules/accounts/models/EnableTwoFactorAuthForm.php b/api/modules/accounts/models/EnableTwoFactorAuthForm.php index 1ed95e1..b3f7b30 100644 --- a/api/modules/accounts/models/EnableTwoFactorAuthForm.php +++ b/api/modules/accounts/models/EnableTwoFactorAuthForm.php @@ -1,4 +1,6 @@ validateOtpDisabled(...)], ['totp', 'required', 'message' => E::TOTP_REQUIRED], ['totp', TotpValidator::class, 'account' => $this->getAccount()], ['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()], @@ -41,7 +43,7 @@ class EnableTwoFactorAuthForm extends AccountActionForm { return true; } - public function validateOtpDisabled($attribute): void { + private function validateOtpDisabled(string $attribute): void { if ($this->getAccount()->is_otp_enabled) { $this->addError($attribute, E::OTP_ALREADY_ENABLED); } diff --git a/api/modules/authserver/controllers/AuthenticationController.php b/api/modules/authserver/controllers/AuthenticationController.php index 7a44eb7..317a68c 100644 --- a/api/modules/authserver/controllers/AuthenticationController.php +++ b/api/modules/authserver/controllers/AuthenticationController.php @@ -7,7 +7,7 @@ use api\controllers\Controller; use api\modules\authserver\models; use Yii; -class AuthenticationController extends Controller { +final class AuthenticationController extends Controller { public function behaviors(): array { $behaviors = parent::behaviors(); @@ -27,12 +27,11 @@ class AuthenticationController extends Controller { } /** - * @return array * @throws \api\modules\authserver\exceptions\ForbiddenOperationException - * @throws \api\modules\authserver\exceptions\IllegalArgumentException */ 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()); 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 } - /** - * @throws \api\modules\authserver\exceptions\ForbiddenOperationException - * @throws \api\modules\authserver\exceptions\IllegalArgumentException - */ public function actionSignout(): void { $model = new models\SignoutForm(); $model->load(Yii::$app->request->post()); diff --git a/api/modules/authserver/models/AuthenticateData.php b/api/modules/authserver/models/AuthenticateData.php index cf8599e..395d1cb 100644 --- a/api/modules/authserver/models/AuthenticateData.php +++ b/api/modules/authserver/models/AuthenticateData.php @@ -15,6 +15,28 @@ final readonly class AuthenticateData { ) { } + /** + * @return array{ + * accessToken: string, + * clientToken: string, + * selectedProfile: array{ + * id: string, + * name: string, + * }, + * availableProfiles?: array, + * user?: array{ + * id: string, + * username: string, + * properties: array, + * }, + * } + */ public function getResponseData(bool $includeAvailableProfiles = false): array { $uuid = str_replace('-', '', $this->account->uuid); $result = [ diff --git a/api/modules/authserver/models/AuthenticationForm.php b/api/modules/authserver/models/AuthenticationForm.php index dda8ebd..74c6869 100644 --- a/api/modules/authserver/models/AuthenticationForm.php +++ b/api/modules/authserver/models/AuthenticationForm.php @@ -3,43 +3,40 @@ declare(strict_types=1); namespace api\modules\authserver\models; -use api\models\authentication\LoginForm; +use api\components\Tokens\TokensFactory; use api\models\base\ApiForm; use api\modules\authserver\exceptions\ForbiddenOperationException; use api\modules\authserver\Module as Authserver; use api\modules\authserver\validators\ClientTokenValidator; use api\modules\authserver\validators\RequiredValidator; use api\rbac\Permissions as P; -use common\helpers\Error as E; +use common\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\OauthClient; use common\models\OauthSession; use Ramsey\Uuid\Uuid; use Webmozart\Assert\Assert; -use Yii; -use yii\db\Exception; -class AuthenticationForm extends ApiForm { +final class AuthenticationForm extends ApiForm { - /** - * @var string - */ - public $username; + public mixed $username = null; - /** - * @var string - */ - public $password; + public mixed $password = null; - /** - * @var string - */ - public $clientToken; + public mixed $clientToken = null; - /** - * @var string|bool - */ - public $requestUser; + public mixed $requestUser = null; + + public function __construct( + private readonly LoginServiceInterface $loginService, + private readonly TokensFactory $tokensFactory, + array $config = [], + ) { + parent::__construct($config); + } public function rules(): array { return [ @@ -50,9 +47,7 @@ class AuthenticationForm extends ApiForm { } /** - * @return AuthenticateData * @throws ForbiddenOperationException - * @throws Exception */ public function authenticate(): AuthenticateData { // 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}'."); - // The previous authorization server implementation used the nickname field instead of username, - // so we keep such behavior - $attribute = !str_contains($this->username, '@') ? 'nickname' : 'email'; - - $password = $this->password; + $password = (string)$this->password; $totp = null; if (preg_match('/.{8,}:(\d{6})$/', $password, $matches) === 1) { $totp = $matches[1]; @@ -73,47 +64,32 @@ class AuthenticationForm extends ApiForm { login: - $loginForm = new LoginForm(); - $loginForm->login = $this->username; - $loginForm->password = $password; - $loginForm->totp = $totp; + $credentials = new Credentials( + login: (string)$this->username, + password: $password, + totp: $totp, + ); - $isValid = $loginForm->validate(); - // Handle case when user's password matches the template for totp via password - if (!$isValid && $totp !== null && $loginForm->getFirstError('password') === E::PASSWORD_INCORRECT) { - $password = "{$password}:{$totp}"; - $totp = null; - - 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"); + try { + $result = $this->loginService->loginByCredentials($credentials); + } catch (Exceptions\InvalidPasswordException $e) { + if ($totp !== null) { + $password = $this->password; + goto login; } - 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(); - $token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $clientToken); + $token = $this->tokensFactory->createForMinecraftAccount($account, $clientToken); $dataModel = new AuthenticateData($account, $token->toString(), $clientToken, (bool)$this->requestUser); /** @var OauthSession|null $minecraftOauthSession */ $minecraftOauthSession = $account->getOauthSessions() @@ -134,4 +110,15 @@ class AuthenticationForm extends ApiForm { 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.'), + }; + } + } diff --git a/api/modules/authserver/models/SignoutForm.php b/api/modules/authserver/models/SignoutForm.php index cd7afcf..5b12103 100644 --- a/api/modules/authserver/models/SignoutForm.php +++ b/api/modules/authserver/models/SignoutForm.php @@ -3,23 +3,14 @@ declare(strict_types=1); namespace api\modules\authserver\models; -use api\models\authentication\LoginForm; use api\models\base\ApiForm; -use api\modules\authserver\exceptions\ForbiddenOperationException; use api\modules\authserver\validators\RequiredValidator; -use common\helpers\Error as E; -class SignoutForm extends ApiForm { +final class SignoutForm extends ApiForm { - /** - * @var string - */ - public $username; + public mixed $username = null; - /** - * @var string - */ - public $password; + public mixed $password = null; public function rules(): array { return [ @@ -27,32 +18,11 @@ class SignoutForm extends ApiForm { ]; } - /** - * @return bool - * @throws ForbiddenOperationException - * @throws \api\modules\authserver\exceptions\IllegalArgumentException - */ public function signout(): bool { $this->validate(); - $loginForm = new LoginForm(); - $loginForm->login = $this->username; - $loginForm->password = $this->password; - if (!$loginForm->validate()) { - $errors = $loginForm->getFirstErrors(); - if (isset($errors['login']) && $errors['login'] === E::ACCOUNT_BANNED) { - // 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 don't give an error about invalid credentials to eliminate a point through which attackers can brut force passwords. return true; } diff --git a/api/tests/_pages/AuthenticationRoute.php b/api/tests/_pages/AuthenticationRoute.php index 5f7f18d..9d8dba2 100644 --- a/api/tests/_pages/AuthenticationRoute.php +++ b/api/tests/_pages/AuthenticationRoute.php @@ -1,14 +1,10 @@ $login, diff --git a/api/tests/functional/LoginCest.php b/api/tests/functional/LoginCest.php index dcd222d..0d8c20a 100644 --- a/api/tests/functional/LoginCest.php +++ b/api/tests/functional/LoginCest.php @@ -8,7 +8,7 @@ use api\tests\FunctionalTester; use OTPHP\TOTP; // TODO: very outdated tests. Need to rewrite -class LoginCest { +final class LoginCest { public function testLoginEmailOrUsername(FunctionalTester $I): void { $route = new AuthenticationRoute($I); diff --git a/api/tests/functional/authserver/AuthorizationCest.php b/api/tests/functional/authserver/AuthorizationCest.php index a87c628..ed4944e 100644 --- a/api/tests/functional/authserver/AuthorizationCest.php +++ b/api/tests/functional/authserver/AuthorizationCest.php @@ -156,7 +156,7 @@ class AuthorizationCest { $I->canSeeResponseIsJson(); $I->canSeeResponseContainsJson([ '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->canSeeResponseContainsJson([ 'error' => 'ForbiddenOperationException', - 'errorMessage' => 'Invalid credentials. Invalid nickname or password.', + 'errorMessage' => 'Invalid credentials. Invalid username or password.', ]); } diff --git a/api/tests/functional/authserver/SignoutCest.php b/api/tests/functional/authserver/SignoutCest.php index 3e219e5..9859f06 100644 --- a/api/tests/functional/authserver/SignoutCest.php +++ b/api/tests/functional/authserver/SignoutCest.php @@ -6,14 +6,15 @@ namespace api\tests\functional\authserver; use api\tests\functional\_steps\AuthserverSteps; use Codeception\Example; -class SignoutCest { +final class SignoutCest { /** * @example {"login": "admin", "password": "password_0"} * @example {"login": "admin@ely.by", "password": "password_0"} + * + * @param \Codeception\Example $example */ public function signout(AuthserverSteps $I, Example $example): void { - $I->wantTo('signout by nickname and password'); $I->sendPOST('/api/authserver/authentication/signout', [ 'username' => $example['login'], 'password' => $example['password'], @@ -23,7 +24,6 @@ class SignoutCest { } public function wrongArguments(AuthserverSteps $I): void { - $I->wantTo('get error on wrong amount of arguments'); $I->sendPOST('/api/authserver/authentication/signout', [ 'key' => 'value', ]); @@ -36,21 +36,15 @@ class SignoutCest { } public function wrongNicknameAndPassword(AuthserverSteps $I): void { - $I->wantTo('signout by nickname and password with wrong data'); $I->sendPOST('/api/authserver/authentication/signout', [ 'username' => 'nonexistent_user', 'password' => 'nonexistent_password', ]); - $I->canSeeResponseCodeIs(401); - $I->canSeeResponseIsJson(); - $I->canSeeResponseContainsJson([ - 'error' => 'ForbiddenOperationException', - 'errorMessage' => 'Invalid credentials. Invalid nickname or password.', - ]); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseEquals(''); } public function bannedAccount(AuthserverSteps $I): void { - $I->wantTo('signout from banned account'); $I->sendPOST('/api/authserver/authentication/signout', [ 'username' => 'Banned', 'password' => 'password_0', diff --git a/api/tests/unit/models/authentication/LoginFormTest.php b/api/tests/unit/models/authentication/LoginFormTest.php deleted file mode 100644 index f64c93b..0000000 --- a/api/tests/unit/models/authentication/LoginFormTest.php +++ /dev/null @@ -1,122 +0,0 @@ - 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; - } - -} diff --git a/api/tests/unit/models/authentication/LogoutFormTest.php b/api/tests/unit/models/authentication/LogoutFormTest.php deleted file mode 100644 index 38e2028..0000000 --- a/api/tests/unit/models/authentication/LogoutFormTest.php +++ /dev/null @@ -1,37 +0,0 @@ -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(); - } - -} diff --git a/api/tests/unit/modules/accounts/models/DisableTwoFactorAuthFormTest.php b/api/tests/unit/modules/accounts/models/DisableTwoFactorAuthFormTest.php index 2d1f7cf..5f7747d 100644 --- a/api/tests/unit/modules/accounts/models/DisableTwoFactorAuthFormTest.php +++ b/api/tests/unit/modules/accounts/models/DisableTwoFactorAuthFormTest.php @@ -5,10 +5,9 @@ namespace api\tests\unit\modules\accounts\models; use api\modules\accounts\models\DisableTwoFactorAuthForm; use api\tests\unit\TestCase; -use common\helpers\Error as E; use common\models\Account; -class DisableTwoFactorAuthFormTest extends TestCase { +final class DisableTwoFactorAuthFormTest extends TestCase { public function testPerformAction(): void { $account = $this->createPartialMock(Account::class, ['save']); @@ -26,18 +25,4 @@ class DisableTwoFactorAuthFormTest extends TestCase { $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')); - } - } diff --git a/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php b/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php index 7c02025..090a3a2 100644 --- a/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php +++ b/api/tests/unit/modules/accounts/models/EnableTwoFactorAuthFormTest.php @@ -6,11 +6,10 @@ namespace api\tests\unit\modules\accounts\models; use api\components\User\Component; use api\modules\accounts\models\EnableTwoFactorAuthForm; use api\tests\unit\TestCase; -use common\helpers\Error as E; use common\models\Account; use Yii; -class EnableTwoFactorAuthFormTest extends TestCase { +final class EnableTwoFactorAuthFormTest extends TestCase { public function testPerformAction(): void { $account = $this->createPartialMock(Account::class, ['save']); @@ -30,18 +29,4 @@ class EnableTwoFactorAuthFormTest extends TestCase { $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')); - } - } diff --git a/api/tests/unit/modules/authserver/models/AuthenticationFormTest.php b/api/tests/unit/modules/authserver/models/AuthenticationFormTest.php deleted file mode 100644 index eee445b..0000000 --- a/api/tests/unit/modules/authserver/models/AuthenticationFormTest.php +++ /dev/null @@ -1,116 +0,0 @@ - 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']; - } - -} diff --git a/api/tests/unit/validators/TotpValidatorTest.php b/api/tests/unit/validators/TotpValidatorTest.php index b1d1768..3c0c386 100644 --- a/api/tests/unit/validators/TotpValidatorTest.php +++ b/api/tests/unit/validators/TotpValidatorTest.php @@ -1,15 +1,17 @@ $account]); - $result = $this->callProtected($validator, 'validateValue', 123456); - $this->assertSame([E::TOTP_INCORRECT, []], $result); + $this->assertFalse($validator->validate(123456, $error)); + $this->assertSame(E::TOTP_INCORRECT, $error); - $result = $this->callProtected($validator, 'validateValue', $controlTotp->now()); - $this->assertNull($result); + $error = null; - $result = $this->callProtected($validator, 'validateValue', $controlTotp->at(time() - 31)); - $this->assertNull($result); + $this->assertTrue($validator->validate($controlTotp->now(), $error)); + $this->assertNull($error); - $at = time() - 400; - $validator->timestamp = $at; - $result = $this->callProtected($validator, 'validateValue', $controlTotp->now()); - $this->assertSame([E::TOTP_INCORRECT, []], $result); + $error = null; - $result = $this->callProtected($validator, 'validateValue', $controlTotp->at($at)); - $this->assertNull($result); + // @phpstan-ignore argument.type + $this->assertTrue($validator->validate($controlTotp->at(time() - 31), $error)); + $this->assertNull($error); - $at = fn(): ?int => null; - $validator->timestamp = $at; - $result = $this->callProtected($validator, 'validateValue', $controlTotp->now()); - $this->assertNull($result); + $error = null; - $at = fn(): int => time() - 700; - $validator->timestamp = $at; - $result = $this->callProtected($validator, 'validateValue', $controlTotp->at($at())); - $this->assertNull($result); + $validator->setClock(new FrozenClock(CarbonImmutable::now()->subSeconds(400))); + $this->assertFalse($validator->validate($controlTotp->now(), $error)); + $this->assertSame(E::TOTP_INCORRECT, $error); } } diff --git a/api/validators/TotpValidator.php b/api/validators/TotpValidator.php index 3566364..fc3a243 100644 --- a/api/validators/TotpValidator.php +++ b/api/validators/TotpValidator.php @@ -1,49 +1,52 @@ account === null) { - $this->account = Yii::$app->user->identity; - } 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)) { 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 { try { + // @phpstan-ignore argument.type (it is non empty, its checked in the init method) $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, []]; } } catch (RangeException) { @@ -53,17 +56,4 @@ class TotpValidator extends Validator { 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; - } - } diff --git a/common/components/Authentication/Entities/AuthenticationResult.php b/common/components/Authentication/Entities/AuthenticationResult.php new file mode 100644 index 0000000..667d875 --- /dev/null +++ b/common/components/Authentication/Entities/AuthenticationResult.php @@ -0,0 +1,17 @@ +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(); + } + +} diff --git a/common/components/Authentication/LoginServiceInterface.php b/common/components/Authentication/LoginServiceInterface.php new file mode 100644 index 0000000..0f56052 --- /dev/null +++ b/common/components/Authentication/LoginServiceInterface.php @@ -0,0 +1,19 @@ + common\components\OAuth2\AuthorizationServerFactory::build(...), + common\components\Authentication\LoginServiceInterface::class => common\components\Authentication\LoginService::class, ], ], 'components' => [ diff --git a/common/models/Account.php b/common/models/Account.php index 9ead47e..ca48c8a 100644 --- a/common/models/Account.php +++ b/common/models/Account.php @@ -77,6 +77,10 @@ class Account extends ActiveRecord { } public function validatePassword(string $password, int $passwordHashStrategy = null): bool { + if (empty($password)) { + return false; + } + if ($passwordHashStrategy === null) { $passwordHashStrategy = $this->password_hash_strategy; } diff --git a/common/models/AccountQuery.php b/common/models/AccountQuery.php index bef3002..cb1a3a0 100644 --- a/common/models/AccountQuery.php +++ b/common/models/AccountQuery.php @@ -6,7 +6,7 @@ namespace common\models; use yii\db\ActiveQuery; /** - * @see Account + * @extends \yii\db\ActiveQuery<\common\models\Account> */ class AccountQuery extends ActiveQuery { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index da8516c..699dc44 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -330,36 +330,6 @@ parameters: count: 1 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\\.$#" count: 1 @@ -415,11 +385,6 @@ parameters: count: 1 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\\.$#" count: 1 @@ -860,11 +825,6 @@ parameters: count: 1 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\\.$#" count: 1 @@ -1045,11 +1005,6 @@ parameters: count: 1 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\\.$#" count: 1 @@ -1100,31 +1055,6 @@ parameters: count: 1 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\\.$#" count: 1 @@ -1150,31 +1080,6 @@ parameters: count: 1 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\\.$#" count: 1 @@ -1285,11 +1190,6 @@ parameters: count: 1 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$#" count: 1