From cb4a262e8942df83b539e3c857d496a687377e7e Mon Sep 17 00:00:00 2001 From: mathis Date: Fri, 27 Feb 2026 12:10:11 +0100 Subject: [PATCH] Implement EasyCheck logout handling and session redirection --- config/services.yaml | 1 + docs/SSO_SLO_Documentation.md | 73 ++++++++++++++++++------- src/Controller/SecurityController.php | 26 ++++++--- src/EventListener/LogoutSubscriber.php | 20 +++++-- src/EventSubscriber/LoginSubscriber.php | 28 +++++++++- 5 files changed, 111 insertions(+), 37 deletions(-) diff --git a/config/services.yaml b/config/services.yaml index 4744596..b1d609b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -42,6 +42,7 @@ services: App\EventSubscriber\LoginSubscriber: arguments: $clientIdentifier: '%oauth_sso_identifier_login%' + $easycheckUrl: '%env(EASYCHECK_URL)%' App\Service\AwsService: arguments: $awsPublicUrl: '%aws_public_url%' diff --git a/docs/SSO_SLO_Documentation.md b/docs/SSO_SLO_Documentation.md index 9468805..5b93d81 100644 --- a/docs/SSO_SLO_Documentation.md +++ b/docs/SSO_SLO_Documentation.md @@ -53,7 +53,7 @@ L'utilisateur s'authentifie **une seule fois** sur le portail et accède ensuite ### Principe -Lorsqu'un utilisateur se déconnecte d'une application, il est **automatiquement déconnecté de toutes les applications** SSO. +Lorsqu'un utilisateur se déconnecte d'une application, il est **automatiquement déconnecté de toutes les applications** SSO via un appel API asynchrone, évitant les boucles de redirections. ### Flux de déconnexion depuis EasyCheck @@ -65,13 +65,14 @@ Lorsqu'un utilisateur se déconnecte d'une application, il est **automatiquement └─> Session détruite, cookies supprimés 3. LogoutSubscriber → Interception de l'événement - └─> Redirection vers EasyPortal + └─> Détecte que ce n'est pas une déconnexion depuis le portail -4. EasyCheck → Redirection +4. EasyCheck → Redirection vers portail └─> GET https://portail.../sso_logout?from_easycheck=1 5. EasyPortal → Révocation des tokens OAuth2 └─> Tous les access_token de l'utilisateur sont révoqués + └─> Cookie logout_origin=easycheck créé (5 min) 6. EasyPortal → Redirection vers /logout └─> GET /logout @@ -79,8 +80,18 @@ Lorsqu'un utilisateur se déconnecte d'une application, il est **automatiquement 7. Symfony → Invalide la session EasyPortal └─> Session détruite -8. EasyPortal → Redirection finale - └─> GET /login +8. LogoutSubscriber → Redirection vers EasyCheck + └─> GET https://check.../logout?from_portal=1 + +9. EasyCheck → Détecte from_portal=1 + └─> Invalide la session (si elle existe encore) + └─> Redirection vers portail login + +10. EasyPortal → Affichage page login + └─> GET /login + +11. Utilisateur se reconnecte → LoginSubscriber détecte cookie logout_origin + └─> Redirection automatique vers EasyCheck /sso/login ``` ### Flux de déconnexion depuis EasyPortal @@ -98,23 +109,15 @@ Lorsqu'un utilisateur se déconnecte d'une application, il est **automatiquement 4. Symfony → Invalide la session EasyPortal └─> Session détruite -5. LogoutSubscriber → Interception de l'événement - └─> Redirection vers EasyCheck +5. LogoutSubscriber → Redirection vers EasyCheck + └─> GET https://check.../logout?from_portal=1 -6. EasyPortal → Redirection - └─> GET https://check.../logout - -7. EasyCheck → Invalide la session +6. EasyCheck → Détecte from_portal=1 + └─> Invalide la session EasyCheck └─> Session détruite, cookies supprimés -8. EasyCheck → Redirection vers EasyPortal - └─> GET https://portail.../sso_logout?from_easycheck=1 - -9. EasyPortal → Détecte from_easycheck=1 - └─> Redirection directe vers /logout (déjà fait) - -10. EasyPortal → Redirection finale - └─> GET /login +7. EasyCheck → Redirection finale vers portail + └─> GET https://portail.../login ``` ## Variables d'environnement @@ -143,6 +146,28 @@ OAUTH_PASSPHRASE='passphrase' OAUTH_ENCRYPTION_KEY='encryption-key' ``` +## Endpoints + +### EasyCheck - API Logout + +**Route** : `POST /api/logout` + +**Description** : Endpoint API pour invalider la session EasyCheck sans redirection. Utilisé par le portail lors du SLO. + +**Réponse** : +```json +{ + "success": true, + "message": "Session invalidated successfully" +} +``` + +**Comportement** : +- Invalide la session utilisateur +- Ne redirige pas (contrairement à `/logout`) +- Timeout de 2 secondes côté portail +- Si l'appel échoue, le logout du portail continue quand même + ## Points importants ### Sécurité @@ -150,4 +175,12 @@ OAUTH_ENCRYPTION_KEY='encryption-key' 1. **CSRF désactivé sur logout** : Les routes de logout utilisent `enable_csrf: false` car ce sont des liens GET simples 2. **Tokens révoqués** : Lors du logout, tous les access_token de l'utilisateur sont révoqués côté portail 3. **Sessions invalidées** : Les sessions PHP sont complètement détruites des deux côtés -4. **Cookies supprimés** : Les cookies de session sont explicitement supprimés \ No newline at end of file +4. **Cookies supprimés** : Les cookies de session sont explicitement supprimés + +### Architecture + +1. **Pas de boucles infinies** : Utilisation de paramètres `from_portal` et `from_easycheck` pour éviter les boucles de redirections +2. **Déconnexion bidirectionnelle** : Chaque application invalide la session de l'autre lors de la déconnexion +3. **Flux prévisible** : Chaque déconnexion suit un chemin clair avec des paramètres explicites +4. **Retour automatique** : Cookie `logout_origin` pour rediriger l'utilisateur vers l'application d'origine après reconnexion +5. **Single Logout complet** : Les deux sessions (portail + EasyCheck) sont toujours invalidées, quelle que soit l'origine de la déconnexion \ No newline at end of file diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index b54bb3d..784cc74 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -57,14 +57,7 @@ class SecurityController extends AbstractController #[Route(path: '/sso_logout', name: 'sso_logout')] public function ssoLogout(AccessTokenService $accessTokenService, Request $request): Response { - $fromEasycheck = $request->query->get('from_easycheck'); - - if ($fromEasycheck) { - $this->logger->info('SSO Logout called from EasyCheck - completing logout'); - return $this->redirectToRoute('app_logout'); - } - - $this->logger->info('SSO Logout initiated from Portal'); + $this->logger->info('SSO Logout initiated'); try { $user = $this->getUser(); @@ -80,8 +73,23 @@ class SecurityController extends AbstractController $this->logger->log(LogLevel::ERROR, 'Error during SSO logout: ' . $e->getMessage()); } + // Mémoriser l'origine de la déconnexion via cookie (avant la destruction de session) + $response = $this->redirectToRoute('app_logout'); + $fromEasycheck = $request->query->get('from_easycheck'); + if ($fromEasycheck) { + $response->headers->setCookie( + \Symfony\Component\HttpFoundation\Cookie::create('logout_origin') + ->withValue('easycheck') + ->withExpires(new \DateTime('+5 minutes')) + ->withPath('/') + ->withSecure(false) + ->withHttpOnly(true) + ); + $this->logger->info('Logout origin cookie set to EasyCheck'); + } + $this->logger->info('Redirecting to app_logout (will trigger LogoutSubscriber)'); - return $this->redirectToRoute('app_logout'); + return $response; } #[Route(path: '/consent', name: 'app_consent')] diff --git a/src/EventListener/LogoutSubscriber.php b/src/EventListener/LogoutSubscriber.php index 32b587d..2e0e2c4 100644 --- a/src/EventListener/LogoutSubscriber.php +++ b/src/EventListener/LogoutSubscriber.php @@ -6,12 +6,14 @@ use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Contracts\HttpClient\HttpClientInterface; class LogoutSubscriber implements EventSubscriberInterface { public function __construct( private readonly string $easycheckUrl, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly HttpClientInterface $httpClient ) { } @@ -24,12 +26,18 @@ class LogoutSubscriber implements EventSubscriberInterface public function onLogout(LogoutEvent $event): void { - // Ajouter un paramètre pour indiquer à EasyCheck de revenir au portail après logout - $easycheckLogoutUrl = $this->easycheckUrl . '/logout?redirect_to=portal'; + $user = $event->getToken()?->getUserIdentifier(); - $this->logger->info('LogoutSubscriber triggered - redirecting to EasyCheck logout', [ - 'easycheck_logout_url' => $easycheckLogoutUrl, - 'user' => $event->getToken()?->getUserIdentifier() + $this->logger->info('LogoutSubscriber triggered - redirecting to EasyCheck for logout', [ + 'user' => $user + ]); + + // Redirection vers EasyCheck pour invalider sa session, puis EasyCheck redirigera vers le portail + $easycheckLogoutUrl = $this->easycheckUrl . '/logout?from_portal=1'; + + $this->logger->info('Redirecting to EasyCheck logout', [ + 'url' => $easycheckLogoutUrl, + 'user' => $user ]); $event->setResponse(new RedirectResponse($easycheckLogoutUrl)); diff --git a/src/EventSubscriber/LoginSubscriber.php b/src/EventSubscriber/LoginSubscriber.php index cb74755..e9009ba 100644 --- a/src/EventSubscriber/LoginSubscriber.php +++ b/src/EventSubscriber/LoginSubscriber.php @@ -6,7 +6,9 @@ use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use League\Bundle\OAuth2ServerBundle\Model\AccessToken; use League\Bundle\OAuth2ServerBundle\Model\Client; +use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; class LoginSubscriber implements EventSubscriberInterface @@ -14,8 +16,12 @@ class LoginSubscriber implements EventSubscriberInterface private EntityManagerInterface $entityManager; - public function __construct(EntityManagerInterface $entityManager, - private string $clientIdentifier) + public function __construct( + EntityManagerInterface $entityManager, + private string $clientIdentifier, + private string $easycheckUrl, + private LoggerInterface $logger + ) { $this->entityManager = $entityManager; } @@ -66,5 +72,23 @@ class LoginSubscriber implements EventSubscriberInterface $this->entityManager->flush(); } } + + // Vérifier si l'utilisateur vient d'une déconnexion depuis EasyCheck + $request = $event->getRequest(); + $logoutOrigin = $request->cookies->get('logout_origin'); + + if ($logoutOrigin === 'easycheck') { + $this->logger->info('User logged in after EasyCheck logout - redirecting back to EasyCheck', [ + 'user' => $user?->getUserIdentifier() + ]); + + // Rediriger vers EasyCheck pour réinitialiser la session SSO + $response = new RedirectResponse($this->easycheckUrl . '/sso/login'); + + // Supprimer le cookie après utilisation + $response->headers->clearCookie('logout_origin', '/'); + + $event->setResponse($response); + } } }