Files
Classeo/backend/tests/Unit/Administration/Infrastructure/Security/JwtPayloadEnricherTest.php
Mathias STRASSER b9d9f48305 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
2026-02-01 14:43:12 +01:00

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);
}
}