src/EventSubscriber/SessionIdleTimeoutSubscriber.php line 46

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use App\Security\MainAuthenticationEntryPoint;
  4. use App\Security\AppAuthenticator;
  5. use Psr\Log\LoggerInterface;
  6. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  7. use Symfony\Component\HttpFoundation\JsonResponse;
  8. use Symfony\Component\HttpFoundation\RedirectResponse;
  9. use Symfony\Component\HttpKernel\Event\RequestEvent;
  10. use Symfony\Component\HttpKernel\KernelEvents;
  11. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  12. use Symfony\Component\Security\Core\User\UserInterface;
  13. use Symfony\Component\Security\Http\FirewallMapInterface;
  14. class SessionIdleTimeoutSubscriber implements EventSubscriberInterface
  15. {
  16.     private const LAST_ACTIVITY_KEY 'last_activity';
  17.     private const REMEMBER_ME_COOKIE_NAME 'REMEMBERME';
  18.     private const DEFAULT_SKIPPED_ROUTES = [
  19.         'app_logout',
  20.         'app_logout_itmconnect',
  21.         '_profiler',
  22.         '_wdt',
  23.     ];
  24.     public function __construct(
  25.         private TokenStorageInterface $tokenStorage,
  26.         private FirewallMapInterface $firewallMap,
  27.         private MainAuthenticationEntryPoint $authenticationEntryPoint,
  28.         private LoggerInterface $logger,
  29.         private int $idleTimeout,
  30.         private string $loginRoute AppAuthenticator::LOGIN_ROUTE,
  31.         private array $statefulFirewalls = ['main'],
  32.     ) {
  33.     }
  34.     public static function getSubscribedEvents(): array
  35.     {
  36.         return [
  37.             KernelEvents::REQUEST => ['onKernelRequest', -10],
  38.         ];
  39.     }
  40.     public function onKernelRequest(RequestEvent $event): void
  41.     {
  42.         if (!$event->isMainRequest() || $event->hasResponse() || $this->idleTimeout <= 0) {
  43.             return;
  44.         }
  45.         $request $event->getRequest();
  46.         $route = (string) ($request->attributes->get('_route') ?? '');
  47.         if ($this->shouldSkipRoute($route)) {
  48.             return;
  49.         }
  50.         $firewallConfig $this->firewallMap->getFirewallConfig($request);
  51.         if (null === $firewallConfig || $firewallConfig->isStateless()) {
  52.             return;
  53.         }
  54.         if (!in_array($firewallConfig->getName(), $this->statefulFirewallstrue)) {
  55.             return;
  56.         }
  57.         $token $this->tokenStorage->getToken();
  58.         if (null === $token) {
  59.             return;
  60.         }
  61.         $user $token->getUser();
  62.         if (!$user instanceof UserInterface) {
  63.             return;
  64.         }
  65.         if (!$request->hasSession()) {
  66.             return;
  67.         }
  68.         $session $request->getSession();
  69.         if (!$session->isStarted()) {
  70.             $session->start();
  71.         }
  72.         $now time();
  73.         $lastActivity $session->get(self::LAST_ACTIVITY_KEY);
  74.         if (is_int($lastActivity) && ($now $lastActivity) > $this->idleTimeout) {
  75.             $this->tokenStorage->setToken(null);
  76.             $session->invalidate();
  77.             $this->logger->info('[SECURITY] Session expired due to inactivity.', [
  78.                 'userId' => method_exists($user'getId') ? $user->getId() : null,
  79.                 'email' => method_exists($user'getEmail') ? $user->getEmail() : null,
  80.                 'identifier' => $user->getUserIdentifier(),
  81.                 'firewall' => $firewallConfig->getName(),
  82.                 'route' => $route,
  83.                 'ip' => $request->getClientIp(),
  84.                 'idleTimeoutSeconds' => $this->idleTimeout,
  85.             ]);
  86.             $entryPointResponse $this->authenticationEntryPoint->start($request);
  87.             $loginUrl $entryPointResponse instanceof RedirectResponse
  88.                 ? (string) $entryPointResponse->headers->get('Location')
  89.                 : $this->authenticationEntryPoint->getLoginUrl($request);
  90.             $response $this->buildExpiredResponse(
  91.                 $request->isXmlHttpRequest(),
  92.                 $request->getPreferredFormat(),
  93.                 $loginUrl,
  94.                 $entryPointResponse
  95.             );
  96.             $response->headers->clearCookie(self::REMEMBER_ME_COOKIE_NAME);
  97.             $response->headers->clearCookie(strtolower(self::REMEMBER_ME_COOKIE_NAME));
  98.             $event->setResponse($response);
  99.             return;
  100.         }
  101.         $session->set(self::LAST_ACTIVITY_KEY$now);
  102.     }
  103.     private function shouldSkipRoute(string $route): bool
  104.     {
  105.         if ($route === '') {
  106.             return false;
  107.         }
  108.         if ($route === $this->loginRoute || in_array($routeself::DEFAULT_SKIPPED_ROUTEStrue)) {
  109.             return true;
  110.         }
  111.         return str_starts_with($route'_profiler') || str_starts_with($route'_wdt');
  112.     }
  113.     private function buildExpiredResponse(
  114.         bool $isXmlHttpRequest,
  115.         string $preferredFormat,
  116.         string $loginUrl,
  117.         RedirectResponse $entryPointResponse
  118.     ): JsonResponse|RedirectResponse {
  119.         if ($isXmlHttpRequest || $preferredFormat === 'json') {
  120.             return new JsonResponse([
  121.                 'error' => 'Session expired',
  122.                 'message' => 'Votre session a expire.',
  123.                 'redirect' => $loginUrl,
  124.             ], 401);
  125.         }
  126.         return $entryPointResponse;
  127.     }
  128. }