<?php declare(strict_types=1);
namespace App\Security\TwoFactor;
use App\Security\TwoFactor\AllowedDevice\AllowedDeviceAuthenticator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class TwoFactorAuthSubscriber implements EventSubscriberInterface
{
private bool $twoFactorEnabled;
private TokenStorageInterface $tokenStorage;
private TwoFactorManager $twoFactorManager;
private RouterInterface $router;
private AllowedDeviceAuthenticator $rememberedDeviceAuthenticator;
private const ROUTES = [
'two_factor_setup',
'two_factor_verify',
'two_factor_notice',
'backend_two_factor_setup',
'backend_two_factor_verify',
'backend_two_factor_notice',
];
public function __construct(
TokenStorageInterface $tokenStorage,
TwoFactorManager $twoFactorManager,
RouterInterface $router,
AllowedDeviceAuthenticator $rememberedDeviceAuthenticator,
bool $twoFactorEnabled
)
{
$this->tokenStorage = $tokenStorage;
$this->twoFactorManager = $twoFactorManager;
$this->router = $router;
$this->rememberedDeviceAuthenticator = $rememberedDeviceAuthenticator;
$this->twoFactorEnabled = $twoFactorEnabled;
}
public function onKernelRequest(RequestEvent $event): void
{
if ($event->getRequestType() !== HttpKernelInterface::MAIN_REQUEST) {
return;
}
/*
* Bypass listener when:
* - user is not logged in or capable of two factor auth.
* - requesting any of the two factor routes.
* - satellite has disabled two factor auth.
* - user is using remembered device
*/
if (!$this->twoFactorEnabled) {
return;
}
$token = $this->tokenStorage->getToken();
/** @var TwoFactorUserInterface|null $user */
$user = (null !== $token && $token->getUser() instanceof TwoFactorUserInterface) ? $token->getUser() : null;
if (null === $user) {
return;
}
$route = $event->getRequest()->attributes->get('_route');
if (in_array($route, self::ROUTES, true)) {
return;
}
if ($this->rememberedDeviceAuthenticator->authenticate($event->getRequest())) {
return;
}
/*
* If user is not authenticated, redirect him based on configuration...
*/
if ($this->twoFactorManager->isAuthenticated($user, $event->getRequest())) {
return;
}
if ($user->isTwoFactorVerified()) {
/*
* This means user is fully capable of authenticating via two factor auth...
*/
$event->setResponse(new RedirectResponse(
$this->router->generate('two_factor_verify', ['continue' => $event->getRequest()->getRequestUri()])
));
} else {
/*
* This means satellite requires users to set up 2fa...
* Redirect to notice page which prompts user to set up 2fa...
*/
$event->setResponse(new RedirectResponse(
$this->router->generate('two_factor_notice', ['continue' => $event->getRequest()->getRequestUri()])
));
}
/*
* Do nothing and allow user trough if satellite does not require 2fa.
*/
}
/**
* @return array<string, array<int|string, array<int|string, int|string>|int|string>|string>
*/
public static function getSubscribedEvents(): array
{
return [
RequestEvent::class => 'onKernelRequest',
];
}
}