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/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/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/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
diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php
index e155d2a..b54bb3d 100644
--- a/src/Controller/SecurityController.php
+++ b/src/Controller/SecurityController.php
@@ -48,22 +48,40 @@ 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, 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');
+
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 (will trigger LogoutSubscriber)');
+ return $this->redirectToRoute('app_logout');
}
#[Route(path: '/consent', name: 'app_consent')]
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