feat: Connexion utilisateur avec sécurité renforcée

Implémente la Story 1.4 du système d'authentification avec plusieurs
couches de protection contre les attaques par force brute.

Sécurité backend :
- Authentification JWT avec access token (15min) + refresh token (7j)
- Rotation automatique des refresh tokens avec détection de replay
- Rate limiting progressif par IP (délai Fibonacci après échecs)
- Intégration Cloudflare Turnstile CAPTCHA après 5 tentatives
- Alerte email à l'utilisateur après blocage temporaire
- Isolation multi-tenant (un utilisateur ne peut se connecter que sur
  son établissement)

Frontend :
- Page de connexion avec feedback visuel des délais et erreurs
- Composant TurnstileCaptcha réutilisable
- Gestion d'état auth avec stockage sécurisé des tokens
- Tests E2E Playwright pour login, tenant isolation, et activation

Infrastructure :
- Configuration Symfony Security avec json_login + jwt
- Cache pools séparés (filesystem en test, Redis en prod)
- NullLoginRateLimiter pour environnement de test (évite blocage CI)
- Génération des clés JWT en CI après démarrage du backend
This commit is contained in:
2026-02-01 10:25:25 +01:00
parent 6889c67a44
commit b9d9f48305
93 changed files with 6850 additions and 155 deletions

View File

@@ -12,7 +12,7 @@ use App\Administration\Domain\Model\User\StatutCompte;
use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\UserRepository;
use App\Shared\Infrastructure\Tenant\TenantId;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use Psr\Cache\CacheItemPoolInterface;
@@ -40,8 +40,9 @@ final readonly class CacheUserRepository implements UserRepository
$item->set($this->serialize($user));
$this->usersCache->save($item);
// Save email index for lookup
$emailItem = $this->usersCache->getItem(self::EMAIL_INDEX_PREFIX . $this->normalizeEmail($user->email));
// Save email index for lookup (scoped to tenant)
$emailKey = $this->emailIndexKey($user->email, $user->tenantId);
$emailItem = $this->usersCache->getItem($emailKey);
$emailItem->set((string) $user->id);
$this->usersCache->save($emailItem);
}
@@ -60,9 +61,10 @@ final readonly class CacheUserRepository implements UserRepository
return $this->deserialize($data);
}
public function findByEmail(Email $email): ?User
public function findByEmail(Email $email, TenantId $tenantId): ?User
{
$emailItem = $this->usersCache->getItem(self::EMAIL_INDEX_PREFIX . $this->normalizeEmail($email));
$emailKey = $this->emailIndexKey($email, $tenantId);
$emailItem = $this->usersCache->getItem($emailKey);
if (!$emailItem->isHit()) {
return null;
@@ -159,4 +161,12 @@ final readonly class CacheUserRepository implements UserRepository
{
return strtolower(str_replace(['@', '.'], ['_at_', '_dot_'], (string) $email));
}
/**
* Creates a cache key for email lookup scoped to a tenant.
*/
private function emailIndexKey(Email $email, TenantId $tenantId): string
{
return self::EMAIL_INDEX_PREFIX . $tenantId . ':' . $this->normalizeEmail($email);
}
}