La double authentification (2FA)

Sommaire
Votre authentification fonctionne depuis le TP Comprendre l'authentification. Dans ce TP, nous allons la renforcer, pas à pas, avec une double authentification : après le mot de passe, un code temporaire sera exigé avant d'ouvrir la session.
Un TP « pour aller plus loin »
Ce TP est un bonus du parcours : il s'adresse à ceux qui sont en avance, ou qui veulent pousser la sécurité plus loin. Il n'est pas indispensable pour la suite du fil rouge… mais vous retrouverez une 2FA construite exactement sur ce modèle dans le projet d'AP : le temps investi ici sera du temps gagné là-bas.
Dans ce TP, je vous invite à avoir en parallèle :
Prérequis
Le projet TODO List avec l'authentification du TP Comprendre l'authentification : inscription, connexion avec Auth::login(), middleware CheckAuth. Nous allons modifier directement votre méthode traitementLogin.
Objectifs
À la fin de ce TP vous saurez :
- Expliquer ce qu'apporte un second facteur d'authentification.
- Générer un code temporaire à 6 chiffres, avec expiration et usage unique.
- Insérer une étape intermédiaire dans un parcours de connexion (sans ouvrir la session trop tôt).
Rappel : qu'est-ce qu'une 2FA ?
Vous l'avez forcément déjà vécue : après le mot de passe, le site vous demande un code à 6 chiffres reçu par email, SMS ou application. C'est la double authentification (2FA) : elle combine quelque chose que vous savez (le mot de passe) avec quelque chose que vous possédez (l'accès à la boîte mail ou au téléphone). Un mot de passe volé ne suffit donc plus pour entrer.
Le parcours de connexion devient :
Point important : tant que le code n'est pas validé, l'utilisateur n'est pas connecté (pas de Auth::login()).
Ce que dit OWASP
La 2FA (ou MFA, Multi-Factor Authentication) est la contre-mesure recommandée par l'OWASP contre le vol de mots de passe, dans la catégorie A07 du Top 10. Sa Multifactor Authentication Cheat Sheet détaille les bonnes pratiques : vous allez en implémenter les principales dans ce TP (expiration courte, usage unique, session ouverte seulement après validation).
La migration
Deux colonnes suffisent, comme souvent avec les codes temporaires :
php artisan make:migration add_two_factor_to_utilisateursSchema::table('utilisateurs', function (Blueprint $table) {
$table->string('two_factor_code', 6)->nullable();
$table->dateTime('two_factor_expires_at')->nullable();
});Je vous laisse compléter la méthode down(), lancer php artisan migrate, et ajouter les deux colonnes dans le $fillable du modèle.
Générer le code au login
Dans traitementLogin, quand le mot de passe est correct, nous ne connectons plus l'utilisateur directement. À la place :
if ($estValide) {
// Un code aléatoire à 6 chiffres (str_pad ajoute les zéros de tête : 007243)
$code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$utilisateur->two_factor_code = $code;
$utilisateur->two_factor_expires_at = now()->addMinutes(10);
$utilisateur->save();
// « Envoi » du code : dans ce TP, il est écrit dans le fichier de log
Log::info("Code 2FA pour {$utilisateur->email} : {$code}");
// On mémorise qui est en cours de connexion (mais PAS encore connecté)
$request->session()->put('2fa_email', $utilisateur->email);
return redirect('/verification');
}N'oubliez pas le use Illuminate\Support\Facades\Log;.
Après un login réussi, votre fichier storage/logs/laravel.log se termine par une ligne de ce type, et les deux colonnes sont remplies en base :
[2026-09-12 12:09:44] local.INFO: Code 2FA pour jane@doe.com : 535842sqlite> SELECT id, email, two_factor_code, two_factor_expires_at FROM utilisateurs;
id email two_factor_code two_factor_expires_at
-- ------------ --------------- ---------------------
1 jane@doe.com 535842 2026-09-12 12:19:44
2 john@doe.comEt le vrai envoi ?
En production, ce code partirait par email ou SMS, jamais dans un log. Nous verrons comment envoyer des emails avec Laravel dans le TP Le reset de mot de passe. En attendant, le fichier storage/logs/laravel.log jouera le rôle de la boîte mail. Si vous avez déjà fait le TP reset, vous pouvez d'ailleurs remplacer le Log::info par un envoi via votre EmailHelpers !
Question :
- Pourquoi stocker
2fa_emailen session plutôt que de considérer l'utilisateur comme connecté ?
La page de vérification
À vous de jouer pour cette étape, vous avez tous les outils :
- Deux routes : GET
/verificationet POST/verification. - Une méthode
showVerification: si la session ne contient pas2fa_email, redirigez vers/login(personne n'a rien à faire sur cette page sans avoir passé l'étape du mot de passe). Sinon, affichez la vue. - Une vue avec un formulaire : un champ
code, un bouton,@csrf, et l'affichage de l'erreur avec@error('code').

Vérifier le code
Pour la vérification, je vous donne la requête clé, qui vérifie le code et son expiration en une seule fois :
$email = $request->session()->get('2fa_email');
$utilisateur = Utilisateur::where('email', $email)
->where('two_factor_code', $request->input('code'))
->where('two_factor_expires_at', '>', now())
->first();Je vous laisse écrire la méthode verification(Request $request) :
- Si
$utilisateurestnull: retour au formulaire avecwithErrors(['code' => 'Code invalide ou expiré']). - Sinon, dans l'ordre :
- Invalider le code (les deux colonnes à
null, puissave()). - Nettoyer la session :
$request->session()->forget('2fa_email');. - Connecter l'utilisateur :
Auth::login($utilisateur);puis$request->session()->regenerate();. - Rediriger vers la TODO List avec un message de bienvenue.
- Invalider le code (les deux colonnes à
Point de contrôle
Déroulez le parcours complet :
- Connectez-vous avec votre mot de passe : vous arrivez sur
/verification, sans être connecté (vérifiez :/tododoit encore rediriger vers/login). - Récupérez le code dans
storage/logs/laravel.loget saisissez-le : vous êtes connecté. - Modifiez à la main
two_factor_expires_aten base pour mettre une date passée : le code doit être refusé. - Après une connexion réussie, vérifiez en base : les deux colonnes 2FA sont repassées à
NULL.


Questions :
- Un code à 6 chiffres n'offre que 1 000 000 de possibilités, un script peut toutes les essayer en quelques minutes… Quelles protections rendent malgré tout ce mécanisme sûr ? (il y en a au moins deux dans votre code, et une troisième arrive dans le TP Aller plus loin)
- Que se passe-t-il si l'utilisateur ferme son navigateur entre le mot de passe et la saisie du code ?
Exercice : une 2FA à la carte dans la bibliothèque
Vous êtes en avance ?
Cet exercice reprend le projet ma-bibliotheque utilisé dans l'exercice du TP Comprendre l'authentification. Si vous ne l'avez pas, commencez par là : c'est un bon entraînement avant d'attaquer celui-ci.
Dans la TODO List, tout le monde passe par la 2FA. Beaucoup de sites laissent pourtant le choix : c'est l'utilisateur qui active la double authentification depuis son compte. C'est ce que je vous propose de construire dans la bibliothèque, cette fois sans aide pas à pas. Pour chaque étape, une cible vous indique ce que vous devez obtenir.
C'est à vous de jouer ! Je reste disponible si vous bloquez.
La page « Mon compte »
- Ajoutez à la table
membresles deux colonnes du TP, ainsi qu'une colonnetwo_factor_active(booléen,falsepar défaut). - Créez une page
/mon-compte, réservée aux membres connectés : elle affiche le pseudo, l'email, l'état de la 2FA (activée ou non) et un bouton pour l'activer ou la désactiver. - Le bouton envoie un formulaire en POST, qui inverse l'état de la 2FA du membre connecté puis revient sur
/mon-compteavec un message flash. - Ajoutez un lien « Mon compte » dans le layout, visible uniquement par les membres connectés.
La cible, juste après avoir activé la 2FA :

La connexion en deux temps
- Si la 2FA du membre est désactivée, il est connecté directement, comme avant.
- Si elle est activée, il suit le parcours de ce TP : code à 6 chiffres dans le log, page de vérification, expiration de 10 minutes, usage unique.
- Après la saisie du bon code, le membre arrive sur la liste des livres (et plus sur la TODO List !).
La cible, pour un membre qui a activé sa 2FA, juste après la saisie de son mot de passe :

Besoin d'aide ?
Pas de code ici, seulement la procédure :
- Une migration
add_two_factor_to_membresavec les trois colonnes ($table->boolean('two_factor_active')->default(false);pour la dernière), puisphp artisan migrate. N'oubliez pas le$fillable. Auth::user()retourne le membre connecté : dans la méthode qui traite le bouton, modifiez sontwo_factor_activepuissave(), comme n'importe quel objet Eloquent.- Dans
traitementLogin, une fois le mot de passe vérifié, c'est$membre->two_factor_activequi décide : connexion directe, ou génération du code et redirection vers/verification. - Les routes
/mon-compte(GET et POST) passent par votre middleware.
Une fois la page en place, testez les deux parcours de connexion, c'est là que se cachent les oublis.
Point de contrôle
Créez deux membres, puis activez la 2FA pour l'un des deux seulement :
- Le membre sans 2FA se connecte directement et arrive sur la bibliothèque.
- Le membre avec 2FA arrive sur
/verificationet doit saisir le code trouvé dansstorage/logs/laravel.log. - Dans votre outil SQLite,
two_factor_activevaut1pour le second membre et0pour le premier.
Questions :
- Pourquoi le bouton d'activation passe-t-il par un formulaire POST (avec
@csrf), plutôt que par un simple lien/mon-compte/2faen GET ? - Désactiver la 2FA d'un simple clic, sans rien redemander, est-ce raisonnable ? Que pourrait faire quelqu'un qui trouve votre session ouverte sur un ordinateur du lycée ?
Les réponses
- Un lien GET peut être déclenché à votre insu : il suffit qu'on vous fasse cliquer sur un lien piégé (dans un email, un message, un autre site) qui pointe vers
/mon-compte/2fa, et votre navigateur envoie la requête avec votre cookie de session. Une action qui modifie des données passe donc toujours en POST, et le jeton@csrfgarantit que le formulaire vient bien de votre site. - Non : il pourrait désactiver votre 2FA, puis se reconnecter plus tard avec votre seul mot de passe (s'il le connaît). Les vrais sites redemandent le mot de passe (ou un code) avant de désactiver une protection. Si vous voulez aller plus loin, ajoutez ce champ « mot de passe actuel » au formulaire de désactivation, et vérifiez-le avec
password_verify.
Conclusion
Votre connexion demande maintenant deux preuves au lieu d'une :
- Un code temporaire généré aléatoirement, jamais deviné.
- Une expiration courte et un usage unique : un code intercepté ne vaut presque rien.
- La session n'est ouverte (
Auth::login()) qu'après la validation du code.
N'oubliez pas de commiter votre projet.
Ce trio « code temporaire + expiration + usage unique » revient dans le TP Le reset de mot de passe, et vous retrouverez une 2FA construite exactement sur ce modèle dans le projet d'AP : vous saurez la lire, la déboguer… et la faire évoluer.