<?php
declare(strict_types=1);
namespace Scheb\TwoFactorBundle\Security\TwoFactor\Provider;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvent;
use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvents;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Exception\UnexpectedTokenException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\KernelEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
/**
* @final
*/
class TwoFactorProviderPreparationListener implements EventSubscriberInterface
{
// This must trigger very first, followed by AuthenticationSuccessEventSuppressor
public const AUTHENTICATION_SUCCESS_LISTENER_PRIORITY = PHP_INT_MAX;
// Execute right before ContextListener, which is serializing the security token into the session
public const RESPONSE_LISTENER_PRIORITY = 1;
/** @deprecated */
public const LISTENER_PRIORITY = self::AUTHENTICATION_SUCCESS_LISTENER_PRIORITY;
/**
* @var TwoFactorProviderRegistry
*/
private $providerRegistry;
/**
* @var PreparationRecorderInterface
*/
private $preparationRecorder;
/**
* @var TwoFactorTokenInterface|null
*/
private $twoFactorToken;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var string
*/
private $firewallName;
/**
* @var bool
*/
private $prepareOnLogin;
/**
* @var bool
*/
private $prepareOnAccessDenied;
public function __construct(
TwoFactorProviderRegistry $providerRegistry,
PreparationRecorderInterface $preparationRecorder,
?LoggerInterface $logger,
string $firewallName,
bool $prepareOnLogin,
bool $prepareOnAccessDenied
) {
$this->providerRegistry = $providerRegistry;
$this->preparationRecorder = $preparationRecorder;
$this->logger = $logger ?? new NullLogger();
$this->firewallName = $firewallName;
$this->prepareOnLogin = $prepareOnLogin;
$this->prepareOnAccessDenied = $prepareOnAccessDenied;
}
public function onLogin(AuthenticationEvent $event): void
{
$token = $event->getAuthenticationToken();
if ($this->prepareOnLogin && $this->supports($token)) {
/** @var TwoFactorTokenInterface $token */
// After login, when the token is a TwoFactorTokenInterface, execute preparation
$this->twoFactorToken = $token;
}
}
public function onAccessDenied(TwoFactorAuthenticationEvent $event): void
{
$token = $event->getToken();
if ($this->prepareOnAccessDenied && $this->supports($token)) {
/** @var TwoFactorTokenInterface $token */
// Whenever two-factor authentication is required, execute preparation
$this->twoFactorToken = $token;
}
}
public function onTwoFactorForm(TwoFactorAuthenticationEvent $event): void
{
$token = $event->getToken();
if ($this->supports($token)) {
/** @var TwoFactorTokenInterface $token */
// Whenever two-factor authentication form is shown, execute preparation
$this->twoFactorToken = $token;
}
}
public function onKernelResponse(ResponseEvent $event): void
{
// Compatibility for Symfony >= 5.3
if (method_exists(KernelEvent::class, 'isMainRequest')) {
if (!$event->isMainRequest()) {
return;
}
} else {
if (!$event->isMasterRequest()) {
return;
}
}
// Unset the token from context. This is important for environments where this instance of the class is reused
// for multiple requests, such as PHP PM.
$twoFactorToken = $this->twoFactorToken;
$this->twoFactorToken = null;
if (!($twoFactorToken instanceof TwoFactorTokenInterface)) {
return;
}
$providerName = $twoFactorToken->getCurrentTwoFactorProvider();
if (null === $providerName) {
return;
}
$firewallName = $twoFactorToken->getProviderKey(true);
try {
if ($this->preparationRecorder->isTwoFactorProviderPrepared($firewallName, $providerName)) {
$this->logger->info(sprintf('Two-factor provider "%s" was already prepared.', $providerName));
return;
}
$user = $twoFactorToken->getUser();
$this->providerRegistry->getProvider($providerName)->prepareAuthentication($user);
$this->preparationRecorder->setTwoFactorProviderPrepared($firewallName, $providerName);
$this->logger->info(sprintf('Two-factor provider "%s" prepared.', $providerName));
} catch (UnexpectedTokenException $e) {
$this->logger->info(sprintf('Two-factor provider "%s" was not prepared, security token was change within the request.', $providerName));
}
}
private function supports(TokenInterface $token): bool
{
return $token instanceof TwoFactorTokenInterface && $token->getProviderKey(true) === $this->firewallName;
}
public static function getSubscribedEvents()
{
return [
AuthenticationEvents::AUTHENTICATION_SUCCESS => ['onLogin', self::AUTHENTICATION_SUCCESS_LISTENER_PRIORITY],
TwoFactorAuthenticationEvents::REQUIRE => 'onAccessDenied',
TwoFactorAuthenticationEvents::FORM => 'onTwoFactorForm',
KernelEvents::RESPONSE => ['onKernelResponse', self::RESPONSE_LISTENER_PRIORITY],
];
}
}