<?php
namespace App\Core\EventSubscriber;
use App\Core\FeatureFlag\FeatureFlagEnum;
use App\Core\FeatureFlag\FeatureFlagRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
class CsrfCookieSubscriber implements EventSubscriberInterface
{
private const COOKIE_NAME = 'csrf_token';
private const FIREWALL_BACKOFFICE = 'security.firewall.map.context.backoffice';
private const FIREWALL_MAIN = 'security.firewall.map.context.main';
public function __construct(
private readonly CsrfTokenManagerInterface $tokenManager,
private readonly FeatureFlagRepository $featureFlagRepository,
private readonly Security $security,
private readonly string $environment
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => ['onKernelResponse', -5], // After security listeners, before profiling
KernelEvents::CONTROLLER => ['onKernelController', -20], // After CsrfProtectedListener (-10)
];
}
public function onKernelResponse(ResponseEvent $event): void
{
if (in_array($this->environment, ['test', 'phpunit'], true)) {
return;
}
if (!$this->featureFlagRepository->isFeatureEnabled(FeatureFlagEnum::CSRF_COOKIE)) {
return;
}
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
$firewallContext = $request->attributes->get('_firewall_context');
if (!$request->hasSession() ||
!($firewallContext === self::FIREWALL_MAIN ||
$firewallContext === self::FIREWALL_BACKOFFICE
)
) {
return;
}
// Only set cookie for authenticated users (not anonymous)
if (null === $this->security->getToken() || !$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
return;
}
$csrfToken = $this->tokenManager->getToken('app')->getValue();
if ($request->cookies->get(self::COOKIE_NAME) === $csrfToken) {
return;
}
$cookie = new Cookie(
name: self::COOKIE_NAME,
value: $csrfToken,
expire: 0,
path: '/web-api',
domain: null,
secure: true,
httpOnly: true,
raw: false,
sameSite: Cookie::SAMESITE_LAX
);
$response = $event->getResponse();
$response->headers->setCookie($cookie);
}
public function onKernelController(ControllerEvent $event): void
{
if (in_array($this->environment, ['test', 'phpunit'], true)) {
return;
}
if (!$this->featureFlagRepository->isFeatureEnabled(FeatureFlagEnum::CSRF_COOKIE)) {
return;
}
$request = $event->getRequest();
$firewallContext = $request->attributes->get('_firewall_context');
if (!$request->hasSession() ||
!($firewallContext === self::FIREWALL_MAIN ||
$firewallContext === self::FIREWALL_BACKOFFICE
)
) {
return;
}
// Only validate CSRF for authenticated users (not anonymous)
if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
return;
}
/** @var string $cookieToken */
$cookieToken = $request->cookies->get(self::COOKIE_NAME);
// If cookie doesn't exist yet, skip validation (first request - cookie will be created in response)
if (!$cookieToken) {
return;
}
$csrfToken = new CsrfToken('app', $cookieToken);
if (!$this->tokenManager->isTokenValid($csrfToken)) {
throw new AccessDeniedException('CSRF: Invalid token');
}
}
}