src/EventSubscriber/SecuritySubscriber.php line 39

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use App\Entity\User;
  4. use App\Service\LogService;
  5. use Doctrine\ORM\EntityManagerInterface;
  6. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  7. use Symfony\Component\HttpFoundation\RedirectResponse;
  8. use Symfony\Component\HttpKernel\Event\RequestEvent;
  9. use Symfony\Component\HttpKernel\KernelEvents;
  10. use Symfony\Component\Routing\RouterInterface;
  11. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  12. use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
  13. use Symfony\Component\Security\Http\Event\LogoutEvent;
  14. class SecuritySubscriber implements EventSubscriberInterface
  15. {
  16.     public function __construct(
  17.         private TokenStorageInterface  $tokenStorage,
  18.         private EntityManagerInterface $em,
  19.         private RouterInterface        $router,
  20.         private LogService             $logService,
  21.     ) {}
  22.     public static function getSubscribedEvents(): array
  23.     {
  24.         return [
  25.             LoginSuccessEvent::class => 'onLoginSuccess',
  26.             LogoutEvent::class       => 'onLogout',
  27.             KernelEvents::REQUEST    => ['onKernelRequest'6],
  28.         ];
  29.     }
  30.     /**
  31.      * Génère un nouveau token à chaque connexion réussie.
  32.      * L'ancien token (autre navigateur/onglet) devient immédiatement invalide.
  33.      */
  34.     public function onLoginSuccess(LoginSuccessEvent $event): void
  35.     {
  36.         $user $event->getUser();
  37.         if (!$user instanceof User) {
  38.             return;
  39.         }
  40.         $token bin2hex(random_bytes(32));
  41.         $user->setSessionToken($token);
  42.         $this->em->flush();
  43.         $event->getRequest()->getSession()->set('_app_session_token'$token);
  44.         $this->logService->log(
  45.             $user->getLogin(),
  46.             'CONNEXION',
  47.             "Connexion réussie de {$user->getPrenom()} {$user->getNom()}",
  48.             null,
  49.             '/'
  50.         );
  51.         if ($user->isPremiereConnexion() === false) {
  52.             $event->setResponse(new RedirectResponse($this->router->generate('changer_password')));
  53.         }
  54.     }
  55.     /**
  56.      * Log la déconnexion.
  57.      */
  58.     public function onLogout(LogoutEvent $event): void
  59.     {
  60.         $token $event->getToken();
  61.         if ($token === null) {
  62.             return;
  63.         }
  64.         $user $token->getUser();
  65.         if (!$user instanceof User) {
  66.             return;
  67.         }
  68.         $this->logService->log(
  69.             $user->getLogin(),
  70.             'DECONNEXION',
  71.             "Déconnexion de {$user->getPrenom()} {$user->getNom()}",
  72.             null,
  73.             '/deconnexion'
  74.         );
  75.     }
  76.     /**
  77.      * Sur chaque requête, vérifie que le token en session correspond
  78.      * à celui en base. Si non → session expirée (connexion sur un autre poste).
  79.      */
  80.     public function onKernelRequest(RequestEvent $event): void
  81.     {
  82.         if (!$event->isMainRequest()) {
  83.             return;
  84.         }
  85.         $securityToken $this->tokenStorage->getToken();
  86.         if ($securityToken === null) {
  87.             return;
  88.         }
  89.         $user $securityToken->getUser();
  90.         if (!$user instanceof User) {
  91.             return;
  92.         }
  93.         $request      $event->getRequest();
  94.         $sessionToken $request->getSession()->get('_app_session_token');
  95.         // Recharge l'entité depuis la base pour avoir le token à jour
  96.         $this->em->refresh($user);
  97.         $dbToken $user->getSessionToken();
  98.         if ($user->isPremiereConnexion() === false) {
  99.             $exemptPaths = [
  100.                 $this->router->generate('changer_password'),
  101.                 $this->router->generate('deconnexion'),
  102.                 $this->router->generate('avantdeconnexion'),
  103.             ];
  104.             if (!in_array($request->getPathInfo(), $exemptPathstrue)) {
  105.                 $event->setResponse(new RedirectResponse($exemptPaths[0]));
  106.                 return;
  107.             }
  108.         }
  109.         if ($dbToken === null) {
  110.             // Aucun token en base : la politique de session unique ne s'applique
  111.             // pas encore à cet utilisateur (jamais authentifié via ce mécanisme).
  112.             return;
  113.         }
  114.         if ($sessionToken === null) {
  115.             // Cas normal juste après une connexion réussie : le token vient d'être
  116.             // généré et flushé par onLoginSuccess, mais la session (nouvellement
  117.             // migrée par Symfony pour la protection anti-fixation) ne l'a pas encore.
  118.             // On la synchronise ici plutôt que d'expulser l'utilisateur qui vient
  119.             // tout juste de se connecter.
  120.             $request->getSession()->set('_app_session_token'$dbToken);
  121.             return;
  122.         }
  123.         if (!hash_equals($dbToken$sessionToken)) {
  124.             $login $user->getLogin();
  125.             $this->logService->log(
  126.                 $login,
  127.                 'SESSION_EXPIREE',
  128.                 "Session expirée pour {$user->getPrenom()} {$user->getNom()} (connexion depuis un autre appareil)",
  129.                 null,
  130.                 $request->getPathInfo()
  131.             );
  132.             $this->tokenStorage->setToken(null);
  133.             $request->getSession()->invalidate();
  134.             $url $this->router->generate('connexion') . '?expired=1';
  135.             $event->setResponse(new RedirectResponse($url));
  136.         }
  137.     }
  138. }