security: password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' providers: app_users: entity: class: App\Entity\User # Pas de `property: email` : UserRepository implémente # UserLoaderInterface pour maîtriser la levée du filtre # multi-organisation au chargement comme au rechargement. firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false # ─── Delivery API ──────────────────────────────────────────────────── # Stateless, clé API uniquement, aucun cookie. Firewall distinct de la # Management API : les deux n'ont ni le même mode d'authentification, ni # la même surface, ni la même sensibilité à la latence. Les fusionner # ferait porter à chaque lecture de bundle le coût de la session. delivery: pattern: ^/delivery stateless: true provider: app_users entry_point: App\Security\ApiKeyAuthenticator custom_authenticators: - App\Security\ApiKeyAuthenticator # ─── Management API + back-office ──────────────────────────────────── # Session cookie SameSite=Lax plutôt que JWT en localStorage : le SPA est # servi en same-origin, le cookie HttpOnly est donc immunisé contre # l'exfiltration par XSS, et une révocation est immédiate. main: lazy: true provider: app_users entry_point: App\Security\ApiEntryPoint # Deux modes d'authentification cohabitent ici, et c'est nécessaire : # le back-office ouvre une session, le CLI présente une clé API. # ApiKeyAuthenticator::supports() ne répond que si un en-tête # d'autorisation est présent, les deux ne se marchent donc pas dessus. custom_authenticators: - App\Security\ApiKeyAuthenticator json_login: check_path: api_auth_login username_path: email password_path: password success_handler: App\Security\AuthenticationHandler failure_handler: App\Security\AuthenticationHandler logout: path: api_auth_logout login_throttling: max_attempts: 5 interval: '15 minutes' access_control: # Endpoints publics : connexion et acceptation d'invitation. Un invité # n'a par définition pas encore de compte. - { path: ^/api/v1/auth/login$, roles: PUBLIC_ACCESS } # Consultation ET acceptation : celui qui clique sur le lien n'a par # définition pas encore de compte, et il doit pouvoir voir à quoi il est # invité avant de choisir un mot de passe. - { path: ^/api/v1/invitations/, roles: PUBLIC_ACCESS } - { path: ^/api/v1/health$, roles: PUBLIC_ACCESS } - { path: ^/api/docs, roles: PUBLIC_ACCESS } # ROLE_API_KEY et non PUBLIC_ACCESS : sans en-tête d'autorisation, le # point d'entrée du firewall renvoie 401. Aucun endpoint de delivery ne # peut ainsi oublier d'exiger une clé. - { path: ^/delivery, roles: ROLE_API_KEY } # L'administration globale est hors de portée d'une clé API, quelles que # soient ses permissions : une clé vit dans un dépôt et une chaîne # d'intégration continue, elle n'a rien à faire près des comptes. Les # attributs #[IsGranted] des contrôleurs disent déjà la même chose ; cette # ligne est la deuxième serrure, celle qui tient si un contrôleur futur # oublie la première. - { path: ^/api/v1/admin/, roles: ROLE_SUPER_ADMIN } # IS_AUTHENTICATED_FULLY et non ROLE_USER : la Management API accepte deux # identités, un utilisateur du back-office et une clé API. Exiger un rôle # propre à l'une exclurait l'autre. Le tri fin appartient aux Voters, qui # savent raisonner sur les deux. - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } when@test: security: password_hashers: # Réduit le coût du hachage dans les tests. Ne jamais reprendre ces # valeurs ailleurs. Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: algorithm: auto cost: 4 time_cost: 3 memory_cost: 10