Implement EasyCheck logout handling and session redirection

This commit is contained in:
mathis 2026-02-27 12:10:11 +01:00
parent 443c7d67b3
commit cb4a262e89
5 changed files with 111 additions and 37 deletions

View File

@ -42,6 +42,7 @@ services:
App\EventSubscriber\LoginSubscriber: App\EventSubscriber\LoginSubscriber:
arguments: arguments:
$clientIdentifier: '%oauth_sso_identifier_login%' $clientIdentifier: '%oauth_sso_identifier_login%'
$easycheckUrl: '%env(EASYCHECK_URL)%'
App\Service\AwsService: App\Service\AwsService:
arguments: arguments:
$awsPublicUrl: '%aws_public_url%' $awsPublicUrl: '%aws_public_url%'

View File

@ -53,7 +53,7 @@ L'utilisateur s'authentifie **une seule fois** sur le portail et accède ensuite
### Principe ### 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 ### 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 └─> Session détruite, cookies supprimés
3. LogoutSubscriber → Interception de l'événement 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 └─> GET https://portail.../sso_logout?from_easycheck=1
5. EasyPortal → Révocation des tokens OAuth2 5. EasyPortal → Révocation des tokens OAuth2
└─> Tous les access_token de l'utilisateur sont révoqués └─> Tous les access_token de l'utilisateur sont révoqués
└─> Cookie logout_origin=easycheck créé (5 min)
6. EasyPortal → Redirection vers /logout 6. EasyPortal → Redirection vers /logout
└─> GET /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 7. Symfony → Invalide la session EasyPortal
└─> Session détruite └─> Session détruite
8. EasyPortal → Redirection finale 8. LogoutSubscriber → Redirection vers EasyCheck
└─> GET /login └─> 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 ### 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 4. Symfony → Invalide la session EasyPortal
└─> Session détruite └─> Session détruite
5. LogoutSubscriber → Interception de l'événement 5. LogoutSubscriber → Redirection vers EasyCheck
└─> Redirection vers EasyCheck └─> GET https://check.../logout?from_portal=1
6. EasyPortal → Redirection 6. EasyCheck → Détecte from_portal=1
└─> GET https://check.../logout └─> Invalide la session EasyCheck
7. EasyCheck → Invalide la session
└─> Session détruite, cookies supprimés └─> Session détruite, cookies supprimés
8. EasyCheck → Redirection vers EasyPortal 7. EasyCheck → Redirection finale vers portail
└─> GET https://portail.../sso_logout?from_easycheck=1 └─> GET https://portail.../login
9. EasyPortal → Détecte from_easycheck=1
└─> Redirection directe vers /logout (déjà fait)
10. EasyPortal → Redirection finale
└─> GET /login
``` ```
## Variables d'environnement ## Variables d'environnement
@ -143,6 +146,28 @@ OAUTH_PASSPHRASE='passphrase'
OAUTH_ENCRYPTION_KEY='encryption-key' 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 ## Points importants
### Sécurité ### 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 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 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 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 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

View File

@ -57,14 +57,7 @@ class SecurityController extends AbstractController
#[Route(path: '/sso_logout', name: 'sso_logout')] #[Route(path: '/sso_logout', name: 'sso_logout')]
public function ssoLogout(AccessTokenService $accessTokenService, Request $request): Response public function ssoLogout(AccessTokenService $accessTokenService, Request $request): Response
{ {
$fromEasycheck = $request->query->get('from_easycheck'); $this->logger->info('SSO Logout initiated');
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 { try {
$user = $this->getUser(); $user = $this->getUser();
@ -80,8 +73,23 @@ class SecurityController extends AbstractController
$this->logger->log(LogLevel::ERROR, 'Error during SSO logout: ' . $e->getMessage()); $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)'); $this->logger->info('Redirecting to app_logout (will trigger LogoutSubscriber)');
return $this->redirectToRoute('app_logout'); return $response;
} }
#[Route(path: '/consent', name: 'app_consent')] #[Route(path: '/consent', name: 'app_consent')]

View File

@ -6,12 +6,14 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Event\LogoutEvent; use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class LogoutSubscriber implements EventSubscriberInterface class LogoutSubscriber implements EventSubscriberInterface
{ {
public function __construct( public function __construct(
private readonly string $easycheckUrl, 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 public function onLogout(LogoutEvent $event): void
{ {
// Ajouter un paramètre pour indiquer à EasyCheck de revenir au portail après logout $user = $event->getToken()?->getUserIdentifier();
$easycheckLogoutUrl = $this->easycheckUrl . '/logout?redirect_to=portal';
$this->logger->info('LogoutSubscriber triggered - redirecting to EasyCheck logout', [ $this->logger->info('LogoutSubscriber triggered - redirecting to EasyCheck for logout', [
'easycheck_logout_url' => $easycheckLogoutUrl, 'user' => $user
'user' => $event->getToken()?->getUserIdentifier() ]);
// 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)); $event->setResponse(new RedirectResponse($easycheckLogoutUrl));

View File

@ -6,7 +6,9 @@ use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken; use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
use League\Bundle\OAuth2ServerBundle\Model\Client; use League\Bundle\OAuth2ServerBundle\Model\Client;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
class LoginSubscriber implements EventSubscriberInterface class LoginSubscriber implements EventSubscriberInterface
@ -14,8 +16,12 @@ class LoginSubscriber implements EventSubscriberInterface
private EntityManagerInterface $entityManager; private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager, public function __construct(
private string $clientIdentifier) EntityManagerInterface $entityManager,
private string $clientIdentifier,
private string $easycheckUrl,
private LoggerInterface $logger
)
{ {
$this->entityManager = $entityManager; $this->entityManager = $entityManager;
} }
@ -66,5 +72,23 @@ class LoginSubscriber implements EventSubscriberInterface
$this->entityManager->flush(); $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);
}
} }
} }