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