Skip to content

La double authentification (2FA) ​

Laravel

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 :

Le parcours de connexion avec la double authentification

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 :

sh
php artisan make:migration add_two_factor_to_utilisateurs
php
Schema::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 :

php
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 : 535842
sqlite> 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.com

Et 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_email en 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 /verification et POST /verification.
  • Une méthode showVerification : si la session ne contient pas 2fa_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').

La page de vérification du 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 :

php
$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 $utilisateur est null : retour au formulaire avec withErrors(['code' => 'Code invalide ou expiré']).
  • Sinon, dans l'ordre :
    1. Invalider le code (les deux colonnes à null, puis save()).
    2. Nettoyer la session : $request->session()->forget('2fa_email');.
    3. Connecter l'utilisateur : Auth::login($utilisateur); puis $request->session()->regenerate();.
    4. Rediriger vers la TODO List avec un message de bienvenue.

Point de contrôle

Déroulez le parcours complet :

  1. Connectez-vous avec votre mot de passe : vous arrivez sur /verification, sans être connecté (vérifiez : /todo doit encore rediriger vers /login).
  2. Récupérez le code dans storage/logs/laravel.log et saisissez-le : vous êtes connecté.
  3. Modifiez à la main two_factor_expires_at en base pour mettre une date passée : le code doit être refusé.
  4. Après une connexion réussie, vérifiez en base : les deux colonnes 2FA sont repassées à NULL.

Un code faux ou expiré est refusé

Le bon code : connecté, avec le message de bienvenue

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 membres les deux colonnes du TP, ainsi qu'une colonne two_factor_active (booléen, false par 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-compte avec 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 page Mon compte de la bibliothèque, avec l'état de la 2FA et le bouton pour la désactiver

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 :

La page de vérification du code dans la bibliothèque

Besoin d'aide ?

Pas de code ici, seulement la procédure :

  • Une migration add_two_factor_to_membres avec les trois colonnes ($table->boolean('two_factor_active')->default(false); pour la dernière), puis php artisan migrate. N'oubliez pas le $fillable.
  • Auth::user() retourne le membre connecté : dans la méthode qui traite le bouton, modifiez son two_factor_active puis save(), comme n'importe quel objet Eloquent.
  • Dans traitementLogin, une fois le mot de passe vérifié, c'est $membre->two_factor_active qui 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 :

  1. Le membre sans 2FA se connecte directement et arrive sur la bibliothèque.
  2. Le membre avec 2FA arrive sur /verification et doit saisir le code trouvé dans storage/logs/laravel.log.
  3. Dans votre outil SQLite, two_factor_active vaut 1 pour le second membre et 0 pour 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/2fa en 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 @csrf garantit 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.

×

Reformulation

La reformulation (IA exécutée localement dans votre navigateur) peut faire des erreurs. Envisagez de vérifier les informations.