<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\User;
use Psr\Log\LoggerInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvent;
use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\RateLimiter\RateLimiterFactory;
/**
* Security event subscriber for 2FA events.
*
* Handles:
* - Audit logging for 2FA attempts (success/failure)
* - Rate limiting for brute-force protection
*/
class TwoFactorSecuritySubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly RequestStack $requestStack,
private readonly ?RateLimiterFactory $twoFactorLimiter = null,
) {
}
public static function getSubscribedEvents(): array
{
return [
TwoFactorAuthenticationEvents::SUCCESS => ['onTwoFactorSuccess', 0],
TwoFactorAuthenticationEvents::FAILURE => ['onTwoFactorFailure', 0],
TwoFactorAuthenticationEvents::ATTEMPT => ['onTwoFactorAttempt', 10],
TwoFactorAuthenticationEvents::COMPLETE => ['onTwoFactorComplete', 0],
];
}
/**
* Called when 2FA attempt is made (before validation).
* Apply rate limiting here.
*/
public function onTwoFactorAttempt(TwoFactorAuthenticationEvent $event): void
{
$user = $event->getToken()->getUser();
if (!$user instanceof User) {
return;
}
$request = $this->requestStack->getCurrentRequest();
$ip = $request?->getClientIp() ?? 'unknown';
// Rate limiting check
if ($this->twoFactorLimiter !== null) {
$limiterKey = sprintf('2fa_%s_%s', $user->getId(), $ip);
$limiter = $this->twoFactorLimiter->create($limiterKey);
if (!$limiter->consume()->isAccepted()) {
$this->logger->warning('2FA rate limit exceeded', [
'user_id' => $user->getId(),
'email' => $user->getEmail(),
'ip' => $ip,
]);
// The actual blocking should be handled by the rate limiter configuration
// This log entry is for audit purposes
}
}
$this->logger->info('2FA attempt initiated', [
'user_id' => $user->getId(),
'email' => $user->getEmail(),
'ip' => $ip,
'method' => $user->getTwoFactorMethod(),
]);
}
/**
* Called when 2FA code validation succeeds.
*/
public function onTwoFactorSuccess(TwoFactorAuthenticationEvent $event): void
{
$user = $event->getToken()->getUser();
if (!$user instanceof User) {
return;
}
$request = $this->requestStack->getCurrentRequest();
$ip = $request?->getClientIp() ?? 'unknown';
$this->logger->info('2FA authentication successful', [
'user_id' => $user->getId(),
'email' => $user->getEmail(),
'ip' => $ip,
'method' => $user->getTwoFactorMethod(),
'user_agent' => $request?->headers->get('User-Agent'),
]);
}
/**
* Called when 2FA code validation fails.
*/
public function onTwoFactorFailure(TwoFactorAuthenticationEvent $event): void
{
$user = $event->getToken()->getUser();
if (!$user instanceof User) {
return;
}
$request = $this->requestStack->getCurrentRequest();
$ip = $request?->getClientIp() ?? 'unknown';
$this->logger->warning('2FA authentication failed', [
'user_id' => $user->getId(),
'email' => $user->getEmail(),
'ip' => $ip,
'method' => $user->getTwoFactorMethod(),
'user_agent' => $request?->headers->get('User-Agent'),
]);
}
/**
* Called when 2FA process is fully complete (all providers passed).
*/
public function onTwoFactorComplete(TwoFactorAuthenticationEvent $event): void
{
$user = $event->getToken()->getUser();
if (!$user instanceof User) {
return;
}
$request = $this->requestStack->getCurrentRequest();
$ip = $request?->getClientIp() ?? 'unknown';
$this->logger->info('2FA authentication complete - full access granted', [
'user_id' => $user->getId(),
'email' => $user->getEmail(),
'ip' => $ip,
]);
}
}