Chapitre 9.3 — Symfony : firewalls & auth
⏱️ TL;DR — Symfony fournit un composant Security mûr et secure by default — à condition de le comprendre. Le firewall intercepte les requêtes d’une zone (
main,admin) et gère l’authentification via des authenticators (form login, JSON login, access token,remember me…). Lesecurity.yamlest le cœur : il déclare les firewalls, le password hasher (auto= argon2id/bcrypt — rappel Partie 3), les providers d’utilisateurs, et l’access control. Les mots de passe passent parUserPasswordHasherInterface(jamais en clair), la session est gérée par le framework (rotation à la connexion incluse). Le piège : désactiver ces protections « pour que ça marche » (route hors firewall, hasherplaintext,security: false).
🎯 Objectifs
- Comprendre le rôle du firewall et sa configuration dans
security.yaml. - Configurer le password hasher (
auto) et hacher/vérifier viaUserPasswordHasherInterface. - Comprendre les authenticators (form login, etc.) et le cycle d’authentification.
- Éviter les pièges : routes hors firewall, hasher faible,
access_controlmal ordonné.
Le firewall : la porte d’entrée
Dans Symfony, la sécurité d’accès est organisée autour des firewalls. Un firewall couvre un ensemble d’URL (défini par un pattern) et décide comment les requêtes de cette zone sont authentifiées. La plupart des applis ont un firewall principal (main) couvrant tout le site, parfois un firewall séparé pour une zone d’API ou d’admin.
# config/packages/security.yaml (structure simplifiee)
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
app_users:
entity: { class: App\Entity\User, property: email }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false # ressources de dev, hors securite
main:
lazy: true
provider: app_users
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true # protection CSRF du formulaire de login
logout:
path: app_logout
remember_me:
secret: '%kernel.secret%'
# ...
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profil, roles: ROLE_USER }Points de sécurité de cette config :
password_hashers: 'auto': Symfony choisit le meilleur algorithme disponible (argon2id/bcrypt) et gère le sel/coût. On ne configure jamaisplaintextou un hachage faible.enable_csrf: truesurform_login: protège le formulaire de connexion contre le CSRF.access_control: les règles d’accès par URL, évaluées dans l’ordre (la première qui matche gagne — piège fréquent, voir plus bas).
Le password hasher : ne jamais toucher au clair
Le hachage des mots de passe (Partie 3) est géré par le composant. À l’inscription/au changement, on passe le mot de passe en clair au hasher, qui renvoie le hash à stocker ; à la connexion, le firewall vérifie automatiquement.
// A l'inscription : hacher via le service (jamais stocker le clair)
public function register(UserPasswordHasherInterface $hasher, Request $request): Response
{
$user = new User();
$user->setEmail($email);
$user->setPassword($hasher->hashPassword($user, $plainPassword)); // argon2id via 'auto'
// ... persister
}Le mot de passe en clair ($plainPassword) ne doit pas être persisté ni loggué. Symfony a d’ailleurs un attribut/#[Ignore] et des conventions pour éviter de sérialiser le clair. La vérification à la connexion est faite par le firewall (comparaison en temps constant incluse) — tu n’écris pas la comparaison toi-même.
⚠️ Piège — Ne configure jamais un hasher
plaintextou un algorithme faible « pour tester » (ça se retrouve en prod), et ne compare jamais les mots de passe à la main. Laisseauto: Symfony gère l’algorithme fort, le sel, le coût, la comparaison en temps constant et le rehash automatique quand le coût évolue. Toute tentative de « faire plus simple » ici casse la sécurité.
Les authenticators : comment on prouve son identité
Symfony (composant Security moderne) gère l’authentification via des authenticators. Les plus courants, activés dans security.yaml :
form_login: connexion par formulaire (email/mot de passe), avecenable_csrf.json_login: connexion par payload JSON (pour une SPA/API sur le même domaine).access_token: authentification par jeton (API), via un handler qui valide le token.remember_me: cookie « se souvenir de moi » (signé) — à configurer avec soin (durée,secret).- Authenticator personnalisé : en implémentant l’interface dédiée pour des cas spécifiques (SSO, magic link…).
Le firewall orchestre le tout : il déclenche l’authenticator, gère l’échec (retour au login), le succès (création de la session, régénération de l’id de session — anti-fixation, Partie 4), et expose l’utilisateur courant ($this->getUser(), #[CurrentUser]).
💡 Réflexe — Utilise les authenticators fournis plutôt que de réinventer l’authentification. Ils intègrent déjà les bonnes pratiques (CSRF sur le login, rotation de session, gestion des échecs, protection contre certaines attaques). Pour un besoin spécifique (SSO OIDC — Partie 4, magic link), écris un authenticator personnalisé en t’appuyant sur le composant, plutôt que du code d’auth « à la main » hors framework.
L’access control et son piège d’ordre
access_control protège des URL par rôle/attribut, globalement. Deux points de vigilance :
- L’ordre compte : Symfony applique la première règle dont le
pathmatche, puis s’arrête. Une règle trop large placée avant une plus spécifique peut « avaler » cette dernière.
# ❌ Mauvais ordre : /admin/... matche la 1re regle (ROLE_USER) et s'arrete
access_control:
- { path: ^/admin/public, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_ADMIN } # jamais atteinte pour /admin/public... et OK pour le reste
# Regle : du plus SPECIFIQUE au plus general.access_controlne suffit pas : il protège des zones d’URL (contrôle grossier), mais pas l’accès par objet (l’IDOR de la Partie 5). UnROLE_USERautorisé sur/facturepeut toujours demander la facture d’un autre si le contrôleur ne vérifie pas l’appartenance. C’est le rôle des voters (chapitre 9.4).
🎯 Côté attaquant — Sur une appli Symfony, l’attaquant cherche : des routes hors firewall (ex. une zone d’API oubliée,
security: falsemal placé), unaccess_controlmal ordonné laissant passer une zone censée être protégée, unremember_mefaible, ou — le plus fréquent — un contrôle par URL présent mais aucun contrôle par objet (IDOR). Il teste les endpoints d’admin sans le rôle, et manipule les ids d’objets. Symfony est solide si la config est stricte ; les failles viennent des contournements et des oublis d’autorisation par objet.
🧭 Sur FormaCampus — Le back Symfony de FormaCampus a un firewall
main(site + SPA viajson_loginsur le même domaine) et un firewall d’API pour les intégrations (access_token). Le hasher estauto(argon2id), leform_login/json_loginontenable_csrf, la session est régénérée à la connexion. L’access_controlest ordonné du plus spécifique au plus général et sert de première barrière (zones d’admin enROLE_ADMIN) — mais l’équipe sait qu’il ne remplace pas l’autorisation par objet, assurée par des voters (chapitre suivant). Aucune route sensible n’est ensecurity: false(réservé aux assets de dev).
✏️ Exercices
Exercice 1 — Répare l’access_control. L’ordre suivant laisse /admin/stats accessible à tout utilisateur connecté alors qu’il devrait être réservé aux admins. Pourquoi, et comment corriger ?
access_control:
- { path: ^/admin, roles: ROLE_USER }
- { path: ^/admin/stats, roles: ROLE_ADMIN }✅ Solution
Symfony applique la première règle dont le path matche, puis s’arrête. Or ^/admin matche aussi /admin/stats : la première règle (ROLE_USER) est appliquée, et la seconde (ROLE_ADMIN) n’est jamais atteinte pour cette URL. Résultat : /admin/stats n’exige que ROLE_USER. Correctif : ordonner du plus spécifique au plus général :
access_control:
- { path: ^/admin/stats, roles: ROLE_ADMIN } # specifique d'abord
- { path: ^/admin, roles: ROLE_ADMIN } # (ou ROLE_USER selon le besoin) ensuiteToujours placer les chemins précis avant les préfixes larges.
Exercice 2 — Le hasher de test. Un dev met password_hashers sur plaintext « juste pour débuguer la connexion en local ». Quels risques, et que faire ?
✅ Solution
Risques : (1) les mots de passe sont stockés en clair en base — une fuite les expose tous (et ils sont souvent réutilisés ailleurs) ; (2) ce réglage « de debug » finit en production (config commitée, oubli) — c’est un scénario récurrent d’incident. Il n’y a aucune bonne raison d’utiliser plaintext. Que faire : garder auto partout (argon2id), même en local — la vérification marche pareil. Pour débuguer, on inspecte le flux d’auth (logs, profiler), on ne dégrade jamais le stockage des mots de passe. Un hasher faible n’est pas un outil de debug, c’est une faille en attente.
🧠 Quiz de révision
1. Quel est le rôle d’un firewall dans Symfony ?
Il intercepte les requêtes d’une zone d’URL et décide comment elles sont authentifiées (via des authenticators : form login, json login, access token…). Il gère aussi le succès/échec, la session (régénérée à la connexion) et expose l’utilisateur courant. C’est la porte d’entrée de la sécurité d’accès.
2. Comment configure-t-on le hachage des mots de passe, et comment hache-t-on ?
Via password_hashers: 'auto' dans security.yaml (Symfony choisit argon2id/bcrypt, gère sel/coût/rehash). On hache avec UserPasswordHasherInterface::hashPassword($user, $plain) ; le firewall vérifie à la connexion (temps constant). On ne stocke jamais le clair, on ne compare jamais à la main.
3. Pourquoi activer enable_csrf sur le form login ?
enable_csrf sur le form login ?Pour protéger le formulaire de connexion contre le CSRF (login CSRF, où un attaquant tenterait de connecter la victime à un compte contrôlé). C’est une protection intégrée qu’il suffit d’activer.
4. Quel est le piège de l’ordre dans access_control ?
access_control ?Symfony applique la première règle dont le path matche puis s’arrête. Une règle large (^/admin) placée avant une plus spécifique (^/admin/stats) « avale » cette dernière, qui n’est jamais évaluée. Il faut ordonner du plus spécifique au plus général.
5. Pourquoi access_control ne suffit-il pas à sécuriser l’accès ?
access_control ne suffit-il pas à sécuriser l’accès ?Parce qu’il contrôle des zones d’URL (grossier), mais pas l’accès par objet : un utilisateur autorisé sur /facture peut demander la facture d’un autre (IDOR) si le contrôleur ne vérifie pas l’appartenance. Ce contrôle par objet relève des voters (chapitre 9.4).
Chapitre suivant : Voters & contrôle d’accès — l’autorisation par objet de la Partie 5, implémentée proprement en Symfony pour fermer les IDOR.