<?php
declare(strict_types=1);
namespace SpringerNature\CPS\AMEDReviewTracker\Web\EventSubscriber;
use FOS\UserBundle\Model\UserManagerInterface;
use SpringerNature\CPS\AMEDReviewTracker\App\LockedUserNotifierInterface;
use SpringerNature\CPS\AMEDReviewTracker\Web\Entity\WebUser;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\LockedException;
class LockedUser implements EventSubscriberInterface
{
/**
* @var SessionInterface
*/
private $session;
/**
* @var UserManagerInterface
*/
private $userManager;
/**
* @var LockedUserNotifierInterface
*/
private $notifier;
private const MAX_FAILED_LOGIN_ATTEMPTS = 5;
private const FAILED_LOGIN_TIME_WINDOW_MINUTES = 10;
private const FAILED_LOGINS_KEY = 'failed_logins';
public function __construct(SessionInterface $session, UserManagerInterface $userManager, LockedUserNotifierInterface $notifier)
{
$this->session = $session;
$this->userManager = $userManager;
$this->notifier = $notifier;
}
public static function getSubscribedEvents(): array
{
return [
AuthenticationEvents::AUTHENTICATION_SUCCESS => 'onAuthenticationSuccess',
AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure',
];
}
public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void
{
$username = $event->getAuthenticationToken()->getUsername();
/** @var WebUser $user */
$user = $this->userManager->findUserByUsernameOrEmail($username);
if (null === $user) {
return;
}
if ($user->isAccountLocked()) {
throw new LockedException('Account is locked');
}
}
public function onAuthenticationFailure(AuthenticationFailureEvent $event): void
{
$username = $event->getAuthenticationToken()->getUsername();
/** @var WebUser $user */
$user = $this->userManager->findUserByUsernameOrEmail($username);
if (null === $user) {
return;
}
$this->incrementFailedLoginCounter($username);
$numberOfFailedLogins = $this->getNumberOfFailedLogins($username);
if ($numberOfFailedLogins >= self::MAX_FAILED_LOGIN_ATTEMPTS && ! $user->isAccountLocked()) {
$user->lockAccount();
$this->userManager->updateUser($user);
$this->notifier->__invoke($username);
}
if ($user->isAccountLocked()) {
throw new LockedException('Account is locked');
}
}
private function getNumberOfFailedLogins(string $username): int
{
$failedLogins = $this->session->get(self::FAILED_LOGINS_KEY, []);
$timestamp = time();
if (isset($failedLogins[$username]) && $failedLogins[$username]['expiry'] > $timestamp) {
return $failedLogins[$username]['count'];
}
return 0;
}
private function incrementFailedLoginCounter(string $username): void
{
$failedLogins = $this->session->get(self::FAILED_LOGINS_KEY, []);
$timestamp = time();
if (isset($failedLogins[$username]) && $failedLogins[$username]['expiry'] > $timestamp) {
$failedLogins[$username] = [
'count' => $failedLogins[$username]['count'] + 1,
'expiry' => $failedLogins[$username]['expiry'],
];
} else {
$failedLogins[$username] = [
'count' => 1,
'expiry' => $timestamp + (self::FAILED_LOGIN_TIME_WINDOW_MINUTES * 60),
];
}
$this->session->set(self::FAILED_LOGINS_KEY, $failedLogins);
}
}