src/Core/EventSubscriber/CsrfCookieSubscriber.php line 92

Open in your IDE?
  1. <?php
  2. namespace App\Core\EventSubscriber;
  3. use App\Core\FeatureFlag\FeatureFlagEnum;
  4. use App\Core\FeatureFlag\FeatureFlagRepository;
  5. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  6. use Symfony\Component\HttpFoundation\Cookie;
  7. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  8. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  9. use Symfony\Component\HttpKernel\KernelEvents;
  10. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  11. use Symfony\Component\Security\Core\Security;
  12. use Symfony\Component\Security\Csrf\CsrfToken;
  13. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  14. class CsrfCookieSubscriber implements EventSubscriberInterface
  15. {
  16.     private const COOKIE_NAME 'csrf_token';
  17.     private const FIREWALL_BACKOFFICE 'security.firewall.map.context.backoffice';
  18.     private const FIREWALL_MAIN 'security.firewall.map.context.main';
  19.     public function __construct(
  20.         private readonly CsrfTokenManagerInterface $tokenManager,
  21.         private readonly FeatureFlagRepository     $featureFlagRepository,
  22.         private readonly Security                  $security,
  23.         private readonly string                    $environment
  24.     ) {
  25.     }
  26.     public static function getSubscribedEvents(): array
  27.     {
  28.         return [
  29.             KernelEvents::RESPONSE => ['onKernelResponse', -5], // After security listeners, before profiling
  30.             KernelEvents::CONTROLLER => ['onKernelController', -20], // After CsrfProtectedListener (-10)
  31.         ];
  32.     }
  33.     public function onKernelResponse(ResponseEvent $event): void
  34.     {
  35.         if (in_array($this->environment, ['test''phpunit'], true)) {
  36.             return;
  37.         }
  38.         if (!$this->featureFlagRepository->isFeatureEnabled(FeatureFlagEnum::CSRF_COOKIE)) {
  39.             return;
  40.         }
  41.         if (!$event->isMasterRequest()) {
  42.             return;
  43.         }
  44.         $request $event->getRequest();
  45.         $firewallContext $request->attributes->get('_firewall_context');
  46.         if (!$request->hasSession() ||
  47.             !($firewallContext === self::FIREWALL_MAIN ||
  48.                 $firewallContext === self::FIREWALL_BACKOFFICE
  49.             )
  50.         ) {
  51.             return;
  52.         }
  53.         // Only set cookie for authenticated users (not anonymous)
  54.         if (null === $this->security->getToken() || !$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
  55.             return;
  56.         }
  57.         $csrfToken $this->tokenManager->getToken('app')->getValue();
  58.         if ($request->cookies->get(self::COOKIE_NAME) === $csrfToken) {
  59.             return;
  60.         }
  61.         $cookie = new Cookie(
  62.             nameself::COOKIE_NAME,
  63.             value$csrfToken,
  64.             expire0,
  65.             path'/web-api',
  66.             domainnull,
  67.             securetrue,
  68.             httpOnlytrue,
  69.             rawfalse,
  70.             sameSiteCookie::SAMESITE_LAX
  71.         );
  72.         $response $event->getResponse();
  73.         $response->headers->setCookie($cookie);
  74.     }
  75.     public function onKernelController(ControllerEvent $event): void
  76.     {
  77.         if (in_array($this->environment, ['test''phpunit'], true)) {
  78.             return;
  79.         }
  80.         if (!$this->featureFlagRepository->isFeatureEnabled(FeatureFlagEnum::CSRF_COOKIE)) {
  81.             return;
  82.         }
  83.         $request $event->getRequest();
  84.         $firewallContext $request->attributes->get('_firewall_context');
  85.         if (!$request->hasSession() ||
  86.             !($firewallContext === self::FIREWALL_MAIN ||
  87.                 $firewallContext === self::FIREWALL_BACKOFFICE
  88.             )
  89.         ) {
  90.             return;
  91.         }
  92.         // Only validate CSRF for authenticated users (not anonymous)
  93.         if (!$this->security->isGranted('IS_AUTHENTICATED_FULLY')) {
  94.             return;
  95.         }
  96.         /** @var string $cookieToken */
  97.         $cookieToken $request->cookies->get(self::COOKIE_NAME);
  98.         // If cookie doesn't exist yet, skip validation (first request - cookie will be created in response)
  99.         if (!$cookieToken) {
  100.             return;
  101.         }
  102.         $csrfToken = new CsrfToken('app'$cookieToken);
  103.         if (!$this->tokenManager->isTokenValid($csrfToken)) {
  104.             throw new AccessDeniedException('CSRF: Invalid token');
  105.         }
  106.     }
  107. }