feat: Gestion des sessions utilisateur

Permet aux utilisateurs de visualiser et gérer leurs sessions actives
sur différents appareils, avec la possibilité de révoquer des sessions
à distance en cas de suspicion d'activité non autorisée.

Fonctionnalités :
- Liste des sessions actives avec métadonnées (appareil, navigateur, localisation)
- Identification de la session courante
- Révocation individuelle d'une session
- Révocation de toutes les autres sessions
- Déconnexion avec nettoyage des cookies sur les deux chemins (legacy et actuel)

Sécurité :
- Cache frontend scopé par utilisateur pour éviter les fuites entre comptes
- Validation que le refresh token appartient à l'utilisateur JWT authentifié
- TTL des sessions Redis aligné sur l'expiration du refresh token
- Événements d'audit pour traçabilité (SessionInvalidee, ToutesSessionsInvalidees)

@see Story 1.6 - Gestion des sessions
This commit is contained in:
2026-02-03 10:10:40 +01:00
parent affad287f9
commit b823479658
40 changed files with 4222 additions and 42 deletions

View File

@@ -4,10 +4,14 @@ declare(strict_types=1);
namespace App\Administration\Infrastructure\Security;
use App\Administration\Application\Port\GeoLocationService;
use App\Administration\Application\Service\RefreshTokenManager;
use App\Administration\Domain\Event\ConnexionReussie;
use App\Administration\Domain\Model\RefreshToken\DeviceFingerprint;
use App\Administration\Domain\Model\Session\DeviceInfo;
use App\Administration\Domain\Model\Session\Session;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\SessionRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use App\Shared\Infrastructure\RateLimit\LoginRateLimiterInterface;
@@ -17,14 +21,17 @@ use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* Handles post-login success actions: refresh token, reset rate limit, audit.
* Handles post-login success actions: refresh token, session, reset rate limit, audit.
*
* @see Story 1.4 - T5: Backend Login Endpoint
* @see Story 1.6 - Session management
*/
final readonly class LoginSuccessHandler
{
public function __construct(
private RefreshTokenManager $refreshTokenManager,
private SessionRepository $sessionRepository,
private GeoLocationService $geoLocationService,
private LoginRateLimiterInterface $rateLimiter,
private MessageBusInterface $eventBus,
private Clock $clock,
@@ -62,14 +69,35 @@ final readonly class LoginSuccessHandler
$isMobile,
);
// Create the session with metadata
$deviceInfo = DeviceInfo::fromUserAgent($userAgent);
$location = $this->geoLocationService->locate($ipAddress);
$now = $this->clock->now();
$session = Session::create(
familyId: $refreshToken->familyId,
userId: $userId,
tenantId: $tenantId,
deviceInfo: $deviceInfo,
location: $location,
createdAt: $now,
);
// Calculate TTL (same as refresh token)
$ttlSeconds = $refreshToken->expiresAt->getTimestamp() - $now->getTimestamp();
$this->sessionRepository->save($session, $ttlSeconds);
// Add the refresh token as HttpOnly cookie
// Path is /api to allow session identification on /api/me/sessions endpoints
// Secure flag only on HTTPS (prod), not HTTP (dev)
$isSecure = $request->isSecure();
$cookie = Cookie::create('refresh_token')
->withValue($refreshToken->toTokenString())
->withExpires($refreshToken->expiresAt)
->withPath('/api/token')
->withSecure(true)
->withPath('/api')
->withSecure($isSecure)
->withHttpOnly(true)
->withSameSite('strict');
->withSameSite($isSecure ? 'strict' : 'lax');
$response->headers->setCookie($cookie);