Merge branch 'disconnect' into 'develop'
Implement EasyCheck logout handling and session redirection See merge request easy-solutions/apps/easyportal!41
This commit is contained in:
commit
25a477a8f9
|
|
@ -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%'
|
||||||
|
|
|
||||||
|
|
@ -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 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
|
└─> 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é
|
||||||
|
|
@ -151,3 +176,11 @@ OAUTH_ENCRYPTION_KEY='encryption-key'
|
||||||
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
|
||||||
|
|
@ -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')]
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue