From d50a6bd238d256f0b8a8f6b713a9599cc705761b Mon Sep 17 00:00:00 2001 From: mathis Date: Thu, 26 Feb 2026 17:03:51 +0100 Subject: [PATCH 1/3] implement logout functionality and improve SSO logout process --- config/packages/security.yaml | 7 +++--- src/Controller/SecurityController.php | 31 ++++++++++++++++++--------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 5fe11ff..c0ddc95 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -59,9 +59,10 @@ security: enable_csrf: true default_target_path: app_index use_referer: true -# logout: -# path: app_logout -# target: app_login + logout: + path: app_logout + enable_csrf: false + target: app_login # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index e155d2a..4cc1dbb 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -48,22 +48,33 @@ class SecurityController extends AbstractController ]); } - #[Route(path: '/sso_logout', name: 'sso_logout')] - public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response + #[Route(path: '/logout', name: 'app_logout')] + public function logout(): void { + throw new \Exception('This should never be reached!'); + } + + #[Route(path: '/sso_logout', name: 'sso_logout')] + public function ssoLogout(AccessTokenService $accessTokenService): Response + { + $this->logger->info('SSO Logout called from EasyCheck'); + try { - $user = $this->userService->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); - $id = $user->getId(); - if ($stack->getSession()->invalidate()) { - $accessTokenService->revokeUserTokens($security->getUser()->getUserIdentifier()); - $security->logout(false); + $user = $this->getUser(); + if ($user) { + $id = $user->getId(); + $this->logger->info('Revoking tokens for user', ['user_id' => $id]); + $accessTokenService->revokeUserTokens($user->getUserIdentifier()); $this->loggerService->logUserConnection('User logged out', ['user_id' => $id]); - return $this->redirect('/'); + } else { + $this->logger->warning('No user found during SSO logout'); } } catch (\Exception $e) { - $logger->log(LogLevel::ERROR, 'Error invalidating session: ' . $e->getMessage()); + $this->logger->log(LogLevel::ERROR, 'Error during SSO logout: ' . $e->getMessage()); } - return $this->redirectToRoute('app_index'); + + $this->logger->info('Redirecting to app_logout'); + return $this->redirectToRoute('app_logout'); } #[Route(path: '/consent', name: 'app_consent')] From 5abbd15b4589eb6ad15544a43710c6f7b42cff98 Mon Sep 17 00:00:00 2001 From: mathis Date: Fri, 27 Feb 2026 10:05:00 +0100 Subject: [PATCH 2/3] add logout subscriber and update SSO logout handling --- .env | 4 ++- config/services.yaml | 7 +++++ src/Controller/SecurityController.php | 13 +++++++--- src/EventListener/LogoutSubscriber.php | 36 ++++++++++++++++++++++++++ templates/elements/navbar.html.twig | 2 +- 5 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 src/EventListener/LogoutSubscriber.php diff --git a/.env b/.env index 0f00f8c..9d208c7 100644 --- a/.env +++ b/.env @@ -74,4 +74,6 @@ AWS_ENDPOINT=https://s3.amazonaws.com AWS_S3_PORTAL_URL=https://s3.amazonaws.com/portal ###< aws/aws-sdk-php-symfony ### APP_URL='https://example.com' -APP_DOMAIN='example.com' \ No newline at end of file +APP_DOMAIN='example.com' + +EASYCHECK_URL='https://check.solutions-easy.com' \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index 3ab0764..4744596 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -13,6 +13,7 @@ parameters: logos_directory: '%kernel.project_dir%/public/uploads/logos' oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%' oauth_sso_identifier_login: '%env(OAUTH_SSO_IDENTIFIER_LOGIN)%' + easycheck_url: '%env(EASYCHECK_URL)%' services: # default configuration for services in *this* file @@ -59,3 +60,9 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + App\EventListener\LogoutSubscriber: + arguments: + $easycheckUrl: '%env(EASYCHECK_URL)%' + tags: + - { name: kernel.event_subscriber } diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 4cc1dbb..b54bb3d 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -55,9 +55,16 @@ class SecurityController extends AbstractController } #[Route(path: '/sso_logout', name: 'sso_logout')] - public function ssoLogout(AccessTokenService $accessTokenService): Response + public function ssoLogout(AccessTokenService $accessTokenService, Request $request): Response { - $this->logger->info('SSO Logout called from EasyCheck'); + $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'); try { $user = $this->getUser(); @@ -73,7 +80,7 @@ class SecurityController extends AbstractController $this->logger->log(LogLevel::ERROR, 'Error during SSO logout: ' . $e->getMessage()); } - $this->logger->info('Redirecting to app_logout'); + $this->logger->info('Redirecting to app_logout (will trigger LogoutSubscriber)'); return $this->redirectToRoute('app_logout'); } diff --git a/src/EventListener/LogoutSubscriber.php b/src/EventListener/LogoutSubscriber.php new file mode 100644 index 0000000..be38ffa --- /dev/null +++ b/src/EventListener/LogoutSubscriber.php @@ -0,0 +1,36 @@ + 'onLogout', + ]; + } + + public function onLogout(LogoutEvent $event): void + { + $easycheckLogoutUrl = $this->easycheckUrl . '/logout'; + + $this->logger->info('LogoutSubscriber triggered - redirecting to EasyCheck logout', [ + 'easycheck_logout_url' => $easycheckLogoutUrl, + 'user' => $event->getToken()?->getUserIdentifier() + ]); + + $event->setResponse(new RedirectResponse($easycheckLogoutUrl)); + } +} diff --git a/templates/elements/navbar.html.twig b/templates/elements/navbar.html.twig index b92e280..e15708f 100644 --- a/templates/elements/navbar.html.twig +++ b/templates/elements/navbar.html.twig @@ -123,7 +123,7 @@ {{ ux_icon('bi:gear', {height: '20px', width: '20px'}) }} Profil - + {{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }} Deconnexion From 1d6b9f08d37d2a77bdde048fa8f5ca6e2c8c17d4 Mon Sep 17 00:00:00 2001 From: mathis Date: Fri, 27 Feb 2026 10:31:48 +0100 Subject: [PATCH 3/3] add SSO/SLO documentation for EasyPortal and EasyCheck integration --- docs/SSO_SLO_Documentation.md | 153 ++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/SSO_SLO_Documentation.md diff --git a/docs/SSO_SLO_Documentation.md b/docs/SSO_SLO_Documentation.md new file mode 100644 index 0000000..9468805 --- /dev/null +++ b/docs/SSO_SLO_Documentation.md @@ -0,0 +1,153 @@ +# Documentation SSO/SLO - EasyPortal & EasyCheck + +## Vue d'ensemble + +Cette documentation décrit l'implémentation du **Single Sign-On (SSO)** et du **Single Logout (SLO)** entre deux applications Symfony : +- **EasyPortal** : Serveur d'autorisation OAuth2 (Identity Provider) +- **EasyCheck** : Application cliente OAuth2 + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ +│ EasyPortal │ │ EasyCheck │ +│ (OAuth2 Server)│◄──────────────────►│ (OAuth2 Client) │ +│ │ │ │ +│ - Authentifie │ │ - Utilise le │ +│ - Émet tokens │ │ token OAuth2 │ +│ - Révoque │ │ - Valide token │ +└─────────────────┘ └─────────────────┘ +``` + +## Single Sign-On (SSO) + +### Principe + +L'utilisateur s'authentifie **une seule fois** sur le portail et accède ensuite à toutes les applications sans re-saisir ses identifiants. + +### Flux d'authentification + +``` +1. Utilisateur → EasyCheck + └─> Pas de session active + +2. EasyCheck → Redirection vers EasyPortal + └─> /authorize?client_id=...&redirect_uri=... + +3. Utilisateur → Connexion sur EasyPortal + └─> Login/Password ou session existante + +4. EasyPortal → Redirection vers EasyCheck + └─> /sso_check?code=AUTHORIZATION_CODE + +5. EasyCheck → Échange du code contre un token + └─> POST /token avec authorization_code + └─> Reçoit access_token + refresh_token + +6. EasyCheck → Création de session locale + └─> Stockage du token en session + └─> Utilisateur connecté +``` + +## Single Logout (SLO) + +### Principe + +Lorsqu'un utilisateur se déconnecte d'une application, il est **automatiquement déconnecté de toutes les applications** SSO. + +### Flux de déconnexion depuis EasyCheck + +``` +1. Utilisateur → Clic "Déconnexion" sur EasyCheck + └─> GET /logout + +2. Symfony → Invalide la session EasyCheck + └─> Session détruite, cookies supprimés + +3. LogoutSubscriber → Interception de l'événement + └─> Redirection vers EasyPortal + +4. EasyCheck → Redirection + └─> 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 + +6. EasyPortal → Redirection vers /logout + └─> GET /logout + +7. Symfony → Invalide la session EasyPortal + └─> Session détruite + +8. EasyPortal → Redirection finale + └─> GET /login +``` + +### Flux de déconnexion depuis EasyPortal + +``` +1. Utilisateur → Clic "Déconnexion" sur EasyPortal + └─> GET /sso_logout + +2. EasyPortal → Révocation des tokens OAuth2 + └─> Tous les access_token de l'utilisateur sont révoqués + +3. EasyPortal → Redirection vers /logout + └─> GET /logout + +4. Symfony → Invalide la session EasyPortal + └─> Session détruite + +5. LogoutSubscriber → Interception de l'événement + └─> Redirection vers EasyCheck + +6. EasyPortal → Redirection + └─> GET https://check.../logout + +7. EasyCheck → Invalide la session + └─> 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 +``` + +## Variables d'environnement + +### EasyCheck (.env) + +```bash +# URL du serveur SSO (EasyPortal) +SSO_URL='https://portail.solutions-easy.moi' + +# Configuration OAuth2 +OAUTH_CLIENT_ID='easycheck-client-id' +OAUTH_CLIENT_SECRET='secret-key' +``` + +### EasyPortal (.env) + +```bash +# URL de l'application cliente (EasyCheck) +EASYCHECK_URL='https://check.solutions-easy.moi' + +# Configuration OAuth2 Server +OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.key +OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.key +OAUTH_PASSPHRASE='passphrase' +OAUTH_ENCRYPTION_KEY='encryption-key' +``` + +## Points importants + +### Sécurité + +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