<?php
/**
* Copyright (c) 2019, MND Next GmbH - www.mndnext.de
*/
namespace App\EventListener;
use App\Entity\User;
use App\Entity\UserActionLog;
use App\Services\MailService;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Router;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
class SecurityListener implements EventSubscriberInterface
{
const MAX_LOGIN_ATTEMPTS = 10;
const ROUTE_LOGIN = 'fos_user_security_check';
const ROUTE_RESET_PASSWORD = 'fos_user_resetting_send_email';
const PERIOD_BLOCK_LOGIN = 'PT1H'; // \DateInterval period for blocking login after to manx login fails
const PERIOD_RESET_ATTEMPTS = 'PT1H'; // \DateInterval period after which time the login attempts counter should be reset
/** @var RouterInterface */
private $router;
/** @var Request */
private $request;
/** @var EntityManagerInterface */
private $em;
/** @var UserManagerInterface */
private $userManager;
/** @var MailService */
private $mailer;
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['checkHoneyPot', 10],
AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure',
SecurityEvents::INTERACTIVE_LOGIN => 'onLogin',
FOSUserEvents::CHANGE_PASSWORD_COMPLETED => 'onChangePassword',
FOSUserEvents::RESETTING_RESET_COMPLETED => 'onResetPasswordFinish',
FOSUserEvents::RESETTING_SEND_EMAIL_COMPLETED => 'onResetPasswordStart'
];
}
public function __construct(RouterInterface $router, RequestStack $request, UserManagerInterface $userManager, EntityManagerInterface $em, MailService $mailer)
{
$this->router = $router;
$this->request = $request->getCurrentRequest();
$this->userManager = $userManager;
$this->em = $em;
$this->mailer = $mailer;
}
public function checkHoneyPot(RequestEvent $event)
{
// Only post routes, first check is for typehint on $this->router
$request = $event->getRequest();
if (!$this->router instanceof Router || !$request->isMethod(Request::METHOD_POST)) {
return;
}
// Only for the login check route
$route = $this->router->matchRequest($request)['_route'] ?? '';
if (self::ROUTE_LOGIN == $route) {
$honeyPot = $this->request->get('_password_repeat');
if ($honeyPot) {
$this->request->request->set('_username', '');
$this->request->request->set('_password', '');
$event->setResponse(new RedirectResponse('/'));
}
}
if (self::ROUTE_RESET_PASSWORD == $route) {
$honeyPot = $this->request->get('username_repeat');
if ($honeyPot) {
$this->request->request->set('username', '');
$event->setResponse(new RedirectResponse('/'));
}
}
/* check not needed as isAccountNonLocked() is automatically checked
$user = $this->userManager->findUserByEmail($this->request->get('_username'));
if ($user) {
if ($user->isAccountNonLocked()) {
throw new AuthenticationException('to many login fails, login blocked for 2 hours' );
}
}
*/
}
public function onAuthenticationFailure(AuthenticationFailureEvent $event)
{
$email = $this->request->get('_username');
$log = new UserActionLog();
$log->setAction(UserActionLog::ACTION_FAILED_LOGIN);
$log->setEmail($email);
$log->setIp($this->request->getClientIp());
$this->em->persist($log);
$this->em->flush();
if (!$email) {
return;
}
/** @var User $user */
$user = $this->userManager->findUserByEmail($email);
if (!$user) {
return;
}
$date = $user->getLastFailedLogin();
if ($date instanceof \DateTime) {
$date->add(new \DateInterval(self::PERIOD_RESET_ATTEMPTS));
$now = new \DateTime();
if ($date <= $now) {
$user->resetLoginAttempts();
}
}
$user->addFailedLoginAttempt();
if ($user->getLoginAttempts() >= self::MAX_LOGIN_ATTEMPTS) {
$user->bockTemporary(new \DateInterval(self::PERIOD_BLOCK_LOGIN));
}
$this->em->persist($user);
$this->em->flush();
}
public function onLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
$user->resetLoginAttempts();
$this->em->persist($user);
$this->em->flush();
}
public function onChangePassword(FilterUserResponseEvent $event)
{
$user = $event->getUser();
$log = new UserActionLog();
$log->setEmail($user->getEmail());
$log->setIp($this->request->getClientIp());
$log->setAction(UserActionLog::ACTION_PWD_CHANGED);
$this->em->persist($log);
$this->em->flush();
$this->mailer->sendPasswordChanged($user);
}
public function onResetPasswordStart(GetResponseUserEvent $event)
{
$user = $event->getUser();
$log = new UserActionLog();
$log->setEmail($user->getEmail());
$log->setIp($this->request->getClientIp());
$log->setAction(UserActionLog::ACTION_PWD_CHANGED);
$this->em->persist($log);
$this->em->flush();
}
public function onResetPasswordFinish(FilterUserResponseEvent $event)
{
$user = $event->getUser();
if ($user->getRegisterState() == User::REGISTERED_RESET) {
$user->setRegisterState(User::REGISTERED_CONFIRMED);
$this->userManager->updateUser($user);
}
}
}