<?php declare(strict_types=1);
namespace App\Security\Subscriber;
use App\Entity\LoginLog;
use App\Entity\User;
use App\RabbitMq\Writer\LoginLogWriter;
use App\Service\LocationProvider;
use Sindrive\RabbitMqTaskBundle\Service\TaskHandler;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\Exception\SessionUnavailableException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class LoginLogSubscriber implements EventSubscriberInterface
{
private TaskHandler $taskHandler;
private LocationProvider $locationProvider;
public function __construct(TaskHandler $taskHandler, LocationProvider $locationProvider)
{
$this->taskHandler = $taskHandler;
$this->locationProvider = $locationProvider;
}
public function onSuccess(LoginSuccessEvent $event): void
{
$user = $event->getUser();
if (!$user instanceof User) {
return;
}
$this->processAuthenticationAttempt($event->getRequest(), 'success', $user, null, null);
}
public function onFailure(LoginFailureEvent $event): void
{
$exception = $event->getException();
$request = $event->getRequest();
$user = null;
$username = $request->get('email');
$password = $request->get('password');
$previousException = $exception->getPrevious();
if ($previousException instanceof DisabledException || $previousException instanceof LockedException) {
$status = 'disabled';
$username = null;
$password = null;
$user = $previousException->getUser();
} elseif ($exception instanceof BadCredentialsException) {
if ($previousException instanceof UserNotFoundException) {
$username = $previousException->getUserIdentifier();
$status = 'wrong username';
} else {
$status = 'wrong password';
}
} elseif ($exception instanceof InvalidCsrfTokenException) {
$status = $exception->getMessage();
} elseif ($exception instanceof SessionUnavailableException) {
$status = $exception->getMessage();
} else {
$status = $exception->getMessage();
}
if (!($user instanceof User)) {
$user = null;
}
if (null !== $username && !is_string($username)) {
throw new UnexpectedTypeException($username, 'string');
}
if (null !== $password && !is_string($password)) {
throw new UnexpectedTypeException($username, 'string');
}
$this->processAuthenticationAttempt($request, $status, $user, $username, $password);
}
/**
* @return array<string, array<int|string, array<int|string, int|string>|int|string>|string>
*/
public static function getSubscribedEvents(): array
{
return [
LoginSuccessEvent::class => 'onSuccess',
LoginFailureEvent::class => 'onFailure',
];
}
private function processAuthenticationAttempt(Request $request, string $status, ?User $user, ?string $username, ?string $password): void
{
$loginLog = new LoginLog();
$loginLog->setStatus($status);
$loginLog->setUserAgent($request->headers->get('user-agent'));
$loginLog->setIp($request->getClientIp());
$loginLog->setCountry($this->locationProvider->getCountry());
$loginLog->setReferrer($request->headers->get('referer'));
$loginLog->setUsername($username);
$loginLog->setPassword($password);
// getId here is right choice, because this entity is not being persisted, but being serialized.
$loginLog->setUserId(null !== $user ? $user->getId() : null);
$this->taskHandler->sendTask(LoginLogWriter::QUEUE_NAME, serialize($loginLog));
}
}