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
93 lines
2.9 KiB
PHP
93 lines
2.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Infrastructure\Security;
|
|
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Administration\Infrastructure\Security\JwtPayloadEnricher;
|
|
use App\Administration\Infrastructure\Security\SecurityUser;
|
|
use App\Shared\Infrastructure\Tenant\TenantId;
|
|
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class JwtPayloadEnricherTest extends TestCase
|
|
{
|
|
private JwtPayloadEnricher $enricher;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->enricher = new JwtPayloadEnricher();
|
|
}
|
|
|
|
#[Test]
|
|
public function onJWTCreatedAddsCustomClaimsToPayload(): void
|
|
{
|
|
$userId = UserId::generate();
|
|
$tenantId = TenantId::fromString('550e8400-e29b-41d4-a716-446655440002');
|
|
|
|
$securityUser = new SecurityUser(
|
|
userId: $userId,
|
|
email: 'user@example.com',
|
|
hashedPassword: 'hashed',
|
|
tenantId: $tenantId,
|
|
roles: ['ROLE_PARENT'],
|
|
);
|
|
|
|
$initialPayload = ['username' => 'user@example.com'];
|
|
$event = new JWTCreatedEvent($initialPayload, $securityUser);
|
|
|
|
$this->enricher->onJWTCreated($event);
|
|
|
|
$payload = $event->getData();
|
|
|
|
self::assertSame((string) $userId, $payload['user_id']);
|
|
self::assertSame((string) $tenantId, $payload['tenant_id']);
|
|
self::assertSame(['ROLE_PARENT'], $payload['roles']);
|
|
}
|
|
|
|
#[Test]
|
|
public function onJWTCreatedPreservesExistingPayloadData(): void
|
|
{
|
|
$securityUser = new SecurityUser(
|
|
userId: UserId::generate(),
|
|
email: 'user@example.com',
|
|
hashedPassword: 'hashed',
|
|
tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440002'),
|
|
roles: ['ROLE_ADMIN'],
|
|
);
|
|
|
|
$initialPayload = [
|
|
'username' => 'user@example.com',
|
|
'iat' => 1706436600,
|
|
'exp' => 1706438400,
|
|
];
|
|
$event = new JWTCreatedEvent($initialPayload, $securityUser);
|
|
|
|
$this->enricher->onJWTCreated($event);
|
|
|
|
$payload = $event->getData();
|
|
|
|
self::assertSame('user@example.com', $payload['username']);
|
|
self::assertSame(1706436600, $payload['iat']);
|
|
self::assertSame(1706438400, $payload['exp']);
|
|
}
|
|
|
|
#[Test]
|
|
public function onJWTCreatedDoesNothingForNonSecurityUser(): void
|
|
{
|
|
$nonSecurityUser = $this->createMock(\Symfony\Component\Security\Core\User\UserInterface::class);
|
|
|
|
$initialPayload = ['username' => 'other@example.com'];
|
|
$event = new JWTCreatedEvent($initialPayload, $nonSecurityUser);
|
|
|
|
$this->enricher->onJWTCreated($event);
|
|
|
|
$payload = $event->getData();
|
|
|
|
// Payload should remain unchanged
|
|
self::assertSame(['username' => 'other@example.com'], $payload);
|
|
}
|
|
}
|