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. (#19)
  • Loading branch information
terabytesoftw committed Dec 8, 2023
1 parent c84b7ab commit 9fc9933
Show file tree
Hide file tree
Showing 14 changed files with 330 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
15 changes: 9 additions & 6 deletions src/Framework/Repository/FinderAccountRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Yii\User\Framework\Repository;

use Yii\CoreLibrary\Repository\FinderRepositoryInterface;
use yii\db\ActiveRecordInterface;
use Yii\User\ActiveRecord\Account;

final class FinderAccountRepository
Expand All @@ -16,17 +15,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: 25 additions & 16 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,31 +52,39 @@ public function behaviors(): array
'roles' => ['?'],
],
],
'denyCallback' => function () {
if ($this->user->getIsGuest() === false) {
return $this->goHome();
}
},
],
];
}

public function actionIndex(): Response|string
{
$account = null;
if ($this->user->getIsGuest() === false) {
return $this->goHome();
}

$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->request instanceof Request &&
$this->request->getIsPost() === true &&
$this->request->post('LoginForm') !== null
$this->loginService->checkAllowedIp($this->request->getUserIP()) === false
) {
$login = $this->request->post('LoginForm')['login'] ?? '';
/** @var Account|null $account */
$account = $this->finderAccountRepository->findByUsernameOrEmail($login);
}
$this->trigger(LoginEvent::IP_NOT_ALLOWED, $event);

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

$this->trigger(LoginEvent::BEFORE_LOGIN, $event);
$this->ajaxValidator->validate($loginForm);
Expand All @@ -84,7 +93,7 @@ public function actionIndex(): Response|string
$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 true;
}

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

0 comments on commit 9fc9933

Please sign in to comment.