Chapitre 10.3 — Login form & hachage des mots de passe
Partie 10 : Sécurité — Chapitre 3/6 Version de référence : Symfony 7.4.
⏱️ TL;DR
- L’entité
UserimplémenteUserInterface(+PasswordAuthenticatedUserInterface) :getUserIdentifier(),getRoles(),getPassword(). On la génère avecmake:user.- Les mots de passe sont hachés (jamais en clair) avec
UserPasswordHasherInterfaceet le hasherauto(bcrypt/argon2, évolutif).- Le login form se génère avec
make:security:form-login(contrôleur + template + authenticator).remember_meajoute un cookie de connexion persistante ; le password upgrade re-hache automatiquement au fil des connexions.- Pour un dev Next.js :
User= votre modèle utilisateur, le hasher =bcrypt/argon2(jamais de mot de passe en clair, jamais de MD5/SHA nu), le login form = la page/logincôté back-office (l’API, elle, utilisera le JWT au chapitre suivant).
🎯 Objectifs
- Créer une entité
Userconforme au composant Security. - Hacher et vérifier des mots de passe correctement.
- Mettre en place un formulaire de connexion (back-office).
- Comprendre
remember_meet le password upgrade.
1. L’entité User
php bin/console make:userLe Maker génère une entité conforme :
// src/Entity/User.php
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[UniqueEntity(fields: ['email'], message: 'Cet email est déjà utilisé.')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private string $email;
#[ORM\Column]
private array $roles = [];
#[ORM\Column]
private string $password; // le HASH, jamais le mot de passe en clair
public function getUserIdentifier(): string { return $this->email; }
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function getPassword(): string { return $this->password; }
public function setPassword(string $hashedPassword): static { $this->password = $hashedPassword; return $this; }
public function eraseCredentials(): void { /* effacer des données sensibles temporaires */ }
}getUserIdentifier(): l’identifiant unique (ici l’email) — celui duprovider(chapitre 10.1).getRoles(): les rôles (chapitre 10.2).getPassword(): le hash stocké.
2. Hacher les mots de passe
Jamais de mot de passe en clair, jamais de MD5/SHA1 nus. On utilise UserPasswordHasherInterface avec le hasher auto (configuré au chapitre 10.1), qui choisit un algorithme robuste (bcrypt ou argon2id) et salé automatiquement.
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
final class UserRegistrar
{
public function __construct(
private readonly UserPasswordHasherInterface $hasher,
private readonly EntityManagerInterface $em,
) {}
public function register(RegisterInput $input): User
{
$user = new User();
$user->setEmail($input->email);
$user->setPassword($this->hasher->hashPassword($user, $input->plainPassword)); // ← hachage
$this->em->persist($user);
$this->em->flush();
return $user;
}
}Pour vérifier un mot de passe (login manuel), on utilise isPasswordValid() — mais en pratique, les authenticators (form_login, json_login) le font pour vous.
⚠️ Piège : ne stockez jamais le
plainPassworden base ni ne le mappez comme colonne. Il vit dans le DTO d’entrée (RegisterInput), le temps du hachage, puis disparaît. Seul le hash est persisté. Et n’inventez pas votre propre hachage : le hasherautode Symfony est l’état de l’art (algorithme, sel, coût), et il évolue (password upgrade, §5).
🐘 Rappel PHP 8+ : sous le capot, le hasher utilise
password_hash()/password_verify()de PHP (bcrypt/argon2). Ces fonctions gèrent le sel et le coût ; ne réimplémentez jamais un hachage « maison ».
3. Le formulaire de connexion (back-office)
php bin/console make:security:form-loginÇa génère un SecurityController (actions login/logout) et un template login.html.twig, et configure le firewall main en form_login (chapitre 10.1).
// src/Controller/SecurityController.php
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
final class SecurityController extends AbstractController
{
#[Route('/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
return $this->render('security/login.html.twig', [
'last_username' => $authenticationUtils->getLastUsername(),
'error' => $authenticationUtils->getLastAuthenticationError(),
]);
}
#[Route('/logout', name: 'app_logout')]
public function logout(): void
{
// interceptée par le firewall (logout) — jamais exécutée
throw new \LogicException('Interceptée par la clé logout du firewall.');
}
}Le template affiche un formulaire postant vers /login (avec token CSRF), et Symfony (via form_login) vérifie l’email + le mot de passe haché, ouvre la session, et redirige. La méthode logout() n’est jamais exécutée : le firewall l’intercepte.
⚠️ Piège : la route
/login(et les assets) doit être accessible sans être connecté — sinon boucle de redirection (« pour te connecter, connecte-toi »). Assurez-vous queaccess_controlautorisePUBLIC_ACCESSsur^/login(et que le firewalldevcouvre les assets).
4. Se souvenir de moi
Le remember_me ajoute un cookie signé qui reconnecte l’utilisateur après expiration de la session :
firewalls:
main:
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 7 jours
samesite: laxUne case « Se souvenir de moi » dans le formulaire (_remember_me) active la fonctionnalité.
5. Password upgrade (re-hachage transparent)
Les algorithmes de hachage évoluent (le coût augmente avec le matériel). Symfony peut re-hacher automatiquement un mot de passe avec un algorithme/coût plus fort lors d’une connexion réussie, si le repository implémente PasswordUpgraderInterface (le Maker le génère). L’utilisateur ne voit rien ; son hash est modernisé en base au passage. C’est une bonne pratique de sécurité gratuite.
✏️ Exercices
Exercice 1. Écrivez la méthode register qui hache le mot de passe d’un nouvel utilisateur à partir d’un RegisterInput.
✅ Solution
public function register(RegisterInput $input): User
{
$user = new User();
$user->setEmail($input->email);
$user->setPassword($this->hasher->hashPassword($user, $input->plainPassword));
$this->em->persist($user);
$this->em->flush();
return $user;
}Le plainPassword vient du DTO (jamais persisté) ; seul le hash (hashPassword) est stocké via setPassword.
Exercice 2. Pourquoi ne jamais stocker le mot de passe en clair ni utiliser MD5/SHA1 ?
✅ Solution
En clair : une fuite de base expose directement tous les mots de passe (souvent réutilisés ailleurs). MD5/SHA1 nus : trop rapides à casser (brute-force/rainbow tables) et sans sel. Le bon choix est un hachage lent, salé et adaptatif (bcrypt/argon2id via le hasher auto), conçu pour résister au brute-force et évoluer avec le matériel (password upgrade). On ne réinvente jamais ce mécanisme.
Exercice 3. Votre page /login provoque une boucle de redirection infinie. Cause probable ?
✅ Solution
/login n’est probablement pas accessible sans être connecté : une règle access_control exige un rôle sur un chemin englobant /login, donc Symfony redirige vers /login… qui redirige vers /login. Correction : autoriser l’accès public à ^/login (roles: PUBLIC_ACCESS) et s’assurer que les assets/CSS sont couverts (firewall dev). La page de connexion doit toujours être atteignable par un anonyme.
🧠 Quiz de révision
1. Quelles interfaces l’entité User implémente-t-elle ?
UserInterface (+ PasswordAuthenticatedUserInterface pour l’auth par mot de passe). Méthodes clés : getUserIdentifier(), getRoles(), getPassword().
2. Comment hache-t-on un mot de passe ?
Avec UserPasswordHasherInterface::hashPassword($user, $plain) et le hasher auto (bcrypt/argon2, salé, adaptatif). Jamais en clair, jamais MD5/SHA nu.
3. Où vit le mot de passe en clair, et où le hash ?
Le plain vit dans le DTO d’entrée, le temps du hachage (jamais persisté). Seul le hash est stocké en base (getPassword).
4. Pourquoi la méthode logout() du contrôleur n’est-elle jamais exécutée ?
logout() du contrôleur n’est-elle jamais exécutée ?Le firewall intercepte la route de déconnexion (clé logout) et gère la déconnexion lui-même ; le code de la méthode n’est jamais atteint.
5. Qu’est-ce que le password upgrade ?
Le re-hachage automatique d’un mot de passe (algorithme/coût plus fort) lors d’une connexion réussie, transparent pour l’utilisateur — une modernisation de sécurité gratuite.
Prochain chapitre : 10.4 — JWT pour l’API — l’authentification stateless du front Next.js.