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:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user