Merge branch 'fix-disconnect' into 'develop'
Fix disconnect See merge request easy-solutions/apps/easyportal!37
This commit is contained in:
commit
4cb447d4ad
2
.env
2
.env
|
|
@ -75,3 +75,5 @@ AWS_S3_PORTAL_URL=https://s3.amazonaws.com/portal
|
||||||
###< aws/aws-sdk-php-symfony ###
|
###< aws/aws-sdk-php-symfony ###
|
||||||
APP_URL='https://example.com'
|
APP_URL='https://example.com'
|
||||||
APP_DOMAIN='example.com'
|
APP_DOMAIN='example.com'
|
||||||
|
|
||||||
|
EASYCHECK_URL='https://check.solutions-easy.com'
|
||||||
|
|
@ -59,9 +59,10 @@ security:
|
||||||
enable_csrf: true
|
enable_csrf: true
|
||||||
default_target_path: app_index
|
default_target_path: app_index
|
||||||
use_referer: true
|
use_referer: true
|
||||||
# logout:
|
logout:
|
||||||
# path: app_logout
|
path: app_logout
|
||||||
# target: app_login
|
enable_csrf: false
|
||||||
|
target: app_login
|
||||||
|
|
||||||
# activate different ways to authenticate
|
# activate different ways to authenticate
|
||||||
# https://symfony.com/doc/current/security.html#the-firewall
|
# https://symfony.com/doc/current/security.html#the-firewall
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ parameters:
|
||||||
logos_directory: '%kernel.project_dir%/public/uploads/logos'
|
logos_directory: '%kernel.project_dir%/public/uploads/logos'
|
||||||
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
|
oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
|
||||||
oauth_sso_identifier_login: '%env(OAUTH_SSO_IDENTIFIER_LOGIN)%'
|
oauth_sso_identifier_login: '%env(OAUTH_SSO_IDENTIFIER_LOGIN)%'
|
||||||
|
easycheck_url: '%env(EASYCHECK_URL)%'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
|
|
@ -59,3 +60,9 @@ services:
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|
||||||
|
App\EventListener\LogoutSubscriber:
|
||||||
|
arguments:
|
||||||
|
$easycheckUrl: '%env(EASYCHECK_URL)%'
|
||||||
|
tags:
|
||||||
|
- { name: kernel.event_subscriber }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -48,22 +48,40 @@ class SecurityController extends AbstractController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/sso_logout', name: 'sso_logout')]
|
#[Route(path: '/logout', name: 'app_logout')]
|
||||||
public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response
|
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 {
|
try {
|
||||||
$user = $this->userService->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
|
$user = $this->getUser();
|
||||||
$id = $user->getId();
|
if ($user) {
|
||||||
if ($stack->getSession()->invalidate()) {
|
$id = $user->getId();
|
||||||
$accessTokenService->revokeUserTokens($security->getUser()->getUserIdentifier());
|
$this->logger->info('Revoking tokens for user', ['user_id' => $id]);
|
||||||
$security->logout(false);
|
$accessTokenService->revokeUserTokens($user->getUserIdentifier());
|
||||||
$this->loggerService->logUserConnection('User logged out', ['user_id' => $id]);
|
$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) {
|
} 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')]
|
#[Route(path: '/consent', name: 'app_consent')]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\EventListener;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||||
|
|
||||||
|
class LogoutSubscriber implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $easycheckUrl,
|
||||||
|
private readonly LoggerInterface $logger
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
LogoutEvent::class => '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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
<i class="me-2">{{ ux_icon('bi:gear', {height: '20px', width: '20px'}) }}</i>
|
<i class="me-2">{{ ux_icon('bi:gear', {height: '20px', width: '20px'}) }}</i>
|
||||||
Profil
|
Profil
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" style="padding-left: 8px;" href="{{ path('sso_logout') }}">
|
<a class="dropdown-item" style="padding-left: 8px;" href="{{ path('sso_logout') }}" data-turbo="false">
|
||||||
<i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i>
|
<i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i>
|
||||||
Deconnexion
|
Deconnexion
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue