Skip to content

Commit

Permalink
Update UserModule configuration and LoginService to allow IP-based lo…
Browse files Browse the repository at this point in the history
…gin restrictions.
  • Loading branch information
terabytesoftw committed Dec 8, 2023
1 parent c84b7ab commit e66431f
Show file tree
Hide file tree
Showing 13 changed files with 156 additions and 51 deletions.
3 changes: 0 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ parameters:
dynamicConstantNames:
- YII_DEBUG
- YII_ENV
- YII_ENV_DEV
- YII_ENV_PROD
- YII_ENV_TEST

level: 5

Expand Down
14 changes: 9 additions & 5 deletions src/Framework/Repository/FinderAccountRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ public function __construct(
) {
}

public function findByEmail(string $email): ActiveRecordInterface|array|null
public function findByEmail(string $email): Account|null
{
return $this->finderRepository->findByOneCondition($this->account, ['email' => $email]);
$account = $this->finderRepository->findByOneCondition($this->account, ['email' => $email]);

return $account instanceof Account ? $account : null;
}

public function findByUsername(string $username): ActiveRecordInterface|array|null
public function findByUsername(string $username): Account|null
{
return $this->finderRepository->findByOneCondition($this->account, ['username' => $username]);
$account = $this->finderRepository->findByOneCondition($this->account, ['username' => $username]);

return $account instanceof Account ? $account : null;
}

public function findByUsernameOrEmail(string $usernameOrEmail): ActiveRecordInterface|array|null
public function findByUsernameOrEmail(string $usernameOrEmail): Account|null
{
return filter_var($usernameOrEmail, FILTER_VALIDATE_EMAIL)
? $this->findByEmail($usernameOrEmail)
Expand Down
2 changes: 1 addition & 1 deletion src/Service/TokenToUrl.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private function register(int $id, int $type): bool
{
$account = $this->finderRepository->findById($this->account, $id);

if ($account === null || $account === []) {
if ($account === null) {
throw new RuntimeException('Invalid user identity.');
}

Expand Down
41 changes: 23 additions & 18 deletions src/UseCase/Login/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ final class LoginController extends Controller
public function __construct(
$id,
Module $module,
private readonly Account $account,
private readonly AjaxValidator $ajaxValidator,
private readonly FinderAccountRepository $finderAccountRepository,
private readonly LoginService $loginService,
private readonly PasswordHasher $passwordHasher,
private readonly User $user,
private readonly UserModule $userModule,
private readonly LoginService $loginService,
array $config = []
) {
parent::__construct($id, $module, $config);
Expand All @@ -51,40 +52,44 @@ public function behaviors(): array
'roles' => ['?'],
],
],
'denyCallback' => function () {
if ($this->user->getIsGuest() === false) {
return $this->goHome();
}
},
],
];
}

public function actionIndex(): Response|string
{
$account = null;

if (
$this->request instanceof Request &&
$this->request->getIsPost() === true &&
$this->request->post('LoginForm') !== null
) {
$login = $this->request->post('LoginForm')['login'] ?? '';
/** @var Account|null $account */
$account = $this->finderAccountRepository->findByUsernameOrEmail($login);
if ($this->user->getIsGuest() === false) {
return $this->goHome();
}

$loginForm = new $this->formModelClass($account, $this->passwordHasher, $this->userModule);
$loginForm = new $this->formModelClass(
$this->account,
$this->finderAccountRepository,
$this->passwordHasher,
$this->userModule,
);
$event = new LoginEvent($loginForm, $this->userModule);

if ($this->userModule->allowLogin === false && $this->userModule->allowLoginByIPs === []) {
$this->trigger(LoginEvent::MODULE_DISABLE, $event);

return $this->goHome();
}

if ($this->userModule->allowLoginByIPs !== [] && !$this->loginService->checkAllowedIp($this->request->userIP)) {
$this->trigger(LoginEvent::IP_NOT_ALLOWED, $event);

return $this->goHome();
}

$this->trigger(LoginEvent::BEFORE_LOGIN, $event);
$this->ajaxValidator->validate($loginForm);

if (
$this->request instanceof Request &&
$loginForm->load($this->request->post()) &&
$loginForm->validate() &&
$this->loginService->run($account, $loginForm->autoLogin())
$this->loginService->run($loginForm)
) {
$this->trigger(LoginEvent::AFTER_LOGIN, $event);

Expand Down
2 changes: 2 additions & 0 deletions src/UseCase/Login/LoginEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ final class LoginEvent extends Event
{
public const AFTER_LOGIN = 'afterLogin';
public const BEFORE_LOGIN = 'beforeLogin';
public const IP_NOT_ALLOWED = 'ipNotAllowed';
public const MODULE_DISABLE = 'moduleDisable';

public function __construct(
public readonly Model $model,
Expand Down
25 changes: 15 additions & 10 deletions src/UseCase/Login/LoginForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Yii;
use yii\base\Model;
use Yii\User\ActiveRecord\Account;
use Yii\User\Framework\Repository\FinderAccountRepository;
use Yii\User\UserModule;
use Yiisoft\Security\PasswordHasher;

Expand All @@ -17,7 +18,8 @@ final class LoginForm extends Model
public int $rememberMe = 0;

public function __construct(
private readonly Account|null $account,
public Account|null $account,
private readonly FinderAccountRepository $finderAccountRepository,
private readonly PasswordHasher $passwordHasher,
private readonly UserModule $userModule,
array $config = []
Expand All @@ -41,13 +43,6 @@ public function rules(): array
['login', 'string', 'min' => 3, 'max' => 255],
['login', 'match', 'pattern' => $this->userModule->usernameRegex],
['login', 'required'],
[
'login',
function (string $attribute): void {
$this->addError($attribute, Yii::t('yii.user', 'Invalid login or password.'));
},
'when' => fn (): bool => $this->account === null,
],
// password validate
['password', 'trim'],
['password', 'string', 'min' => 6, 'max' => 72],
Expand All @@ -57,8 +52,7 @@ function (string $attribute): void {
function (string $attribute): void {
$this->addError($attribute, Yii::t('yii.user', 'Invalid login or password.'));
},
'when' => fn (): bool => $this->account === null ||
$this->passwordHasher->validate($this->password, $this->account->password_hash) === false,
'when' => fn (): bool => $this->validatePassword(),
],
];
}
Expand All @@ -67,4 +61,15 @@ public function autoLogin(): int
{
return $this->rememberMe ?: 0;
}

private function validatePassword(): bool
{
$this->account = $this->finderAccountRepository->findByUsernameOrEmail($this->login);

if ($this->account === null || $this->account->password_hash === null) {
return false;
}

return $this->passwordHasher->validate($this->password, $this->account->password_hash) === false;
}
}
27 changes: 23 additions & 4 deletions src/UseCase/Login/LoginService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,42 @@
namespace Yii\User\UseCase\Login;

use Yii\CoreLibrary\Repository\PersistenceRepositoryInterface;
use Yii\User\ActiveRecord\Account;
use yii\helpers\IpHelper;
use Yii\User\UserModule;
use yii\web\User;

final class LoginService
{
public function __construct(
private readonly PersistenceRepositoryInterface $persistenceRepository,
private readonly User $user
private readonly User $user,
private readonly UserModule $userModule,
) {
}

public function run(Account $account, int $autoLogin = 0): bool
public function run(LoginForm $loginForm): bool
{
if ($this->user->login($account->identity, $autoLogin)) {
if ($loginForm->account->identity === null) {
return false;
}

$account = $loginForm->account;

if ($this->user->login($account->identity, $loginForm->autoLogin())) {
$result = $this->persistenceRepository->updateAtttributes($account, ['last_login_at' => time()]);
}

return $result ?? false;
}

public function checkAllowedIp(string $userIP): bool
{
foreach ($this->userModule->allowLoginByIPs as $allowedIP) {
if (IpHelper::inRange($userIP, $allowedIP)) {
return true;
}
}

return false;
}
}
2 changes: 1 addition & 1 deletion src/UseCase/Login/view/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
)
?>
<?php endif ?>
<?php if ($userModule->register) : ?>
<?php if ($userModule->allowRegister) : ?>
<?=
P::widget()
->class('mt-3 text-center')
Expand Down
2 changes: 1 addition & 1 deletion src/UseCase/Register/RegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function actionIndex(): Response|string
$registerForm = new $this->formModelClass($this->userModule);
$event = new RegisterEvent($registerForm, $this->userModule);

if ($this->userModule->register === false) {
if ($this->userModule->allowRegister === false) {
$this->trigger(RegisterEvent::MODULE_DISABLE, $event);

return $this->goHome();
Expand Down
4 changes: 3 additions & 1 deletion src/UserModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ final class UserModule
public readonly string $urlConfirmation;

public function __construct(
public readonly bool $allowLogin = true,
public readonly array $allowLoginByIPs = [],
public readonly bool $allowRegister = true,
public readonly int $autoLogin = 1209600,
public readonly bool $confirmation = false,
public readonly bool $floatLabels = true,
Expand All @@ -67,7 +70,6 @@ public function __construct(
public readonly array $mailerWelcomeLayout = ['html' => 'welcome', 'text' => 'text/welcome'],
string|null $mailerWelcomeSubject = null,
public readonly bool $passwordRecovery = true,
public readonly bool $register = true,
public readonly bool $showPassword = false,
public readonly int $token2FAWithin = 3600,
public readonly int $tokenConfirmWithin = 86400,
Expand Down
46 changes: 46 additions & 0 deletions tests/Acceptance/LoginCest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,52 @@

final class LoginCest
{
public function allowLogin(AcceptanceTester $I): void
{
$I->amGoingTo('disable login page.');
$I->allowLogin(false);

$I->amGoingTo('go to the page login.');
$I->amOnRoute('login/index');

$I->expectTo('see message home page.');
$I->see(Yii::t('app.basic', 'Web Application'));

$I->amGoingTo('enable login page.');
$I->allowLogin(true);
}

public function allowLoginByIps(AcceptanceTester $I): void
{
$I->amGoingTo('allow login by ips.');
$I->allowLoginByIPs(false, ['127.0.0.1', '127.0.0.2', '127.0.0.3']);

$I->amGoingTo('go to the page login.');
$I->amOnRoute('login/index');

$I->expectTo('see message login page.');
$I->see(Yii::t('yii.user', 'Sign in'), 'h1');
$I->see(Yii::t('yii.user', 'Please fill out the following fields to Sign in.'));

$I->amGoingTo('disable login by ips.');
$I->allowLogin(true, []);
}

public function allowLoginByIpsFailed(AcceptanceTester $I): void
{
$I->amGoingTo('allow login by ips.');
$I->allowLoginByIPs(false, ['172.0.0.1']);

$I->amGoingTo('go to the page login.');
$I->amOnRoute('login/index');

$I->expectTo('see message home page.');
$I->see(Yii::t('app.basic', 'Web Application'));

$I->amGoingTo('disable login by ips.');
$I->allowLogin(true, []);
}

public function indexPage(AcceptanceTester $I): void
{
$I->amGoingTo('navigate to the login page.');
Expand Down
10 changes: 5 additions & 5 deletions tests/Acceptance/RegisterCest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@

final class RegisterCest
{
public function disablePage(AcceptanceTester $I): void
public function allowRegister(AcceptanceTester $I): void
{
$I->amGoingTo('disable register page.');
$I->accountRegister(false);
$I->allowRegister(false);

$I->amGoingTo('go to the page registration.');
$I->amGoingTo('go to the page register.');
$I->amOnRoute('register/index');

$I->expectTo('see message register disabled.');
$I->expectTo('see message home page.');
$I->see(Yii::t('app.basic', 'Web Application'));

$I->amGoingTo('enable register page.');
$I->accountRegister(true);
$I->allowRegister(true);
}

public function indexPage(AcceptanceTester $I): void
Expand Down
29 changes: 27 additions & 2 deletions tests/Support/AcceptanceTester.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,38 @@ public function accountGeneratePassword(bool $option): void
);
}

public function accountRegister(bool $option): void
public function allowLogin(bool $option): void
{
\Yii::$container->set(
\Yii\User\UserModule::class,
[
'__construct()' => [
'register' => $option,
'allowLogin' => $option,
],
],
);
}

public function allowLoginByIPs(bool $allowLogin, array $ips): void
{
\Yii::$container->set(
\Yii\User\UserModule::class,
[
'__construct()' => [
'allowLogin' => $allowLogin,
'allowLoginByIPs' => $ips,
],
],
);
}

public function allowRegister(bool $option): void
{
\Yii::$container->set(
\Yii\User\UserModule::class,
[
'__construct()' => [
'allowRegister' => $option,
],
],
);
Expand Down

0 comments on commit e66431f

Please sign in to comment.