tutorial/2fa-symfony.md

Mettre en place la 2FA dans une application Symfony (bundle Scheb)

Ce tutoriel vous guide, du début jusqu'à un écran de connexion qui exige un second facteur, pour ajouter la double authentification (2FA) à une application Symfony. À la fin, un utilisateur qui a activé la 2FA devra saisir un code reçu par e-mail après son mot de passe — sans être harcelé sur les appareils qu'il a marqués « de confiance ».

Nous utilisons scheb/2fa-bundle, la brique de référence pour la 2FA en Symfony. On avance en un seul chemin, celui qui marche ; une extension TOTP (application d'authentification) est proposée à la fin, une fois l'e-mail en place.

Avant de commencer

Il vous faut :

  • une application Symfony fonctionnelle avec une authentification par e-mail + mot de passe dĂ©jĂ  opĂ©rationnelle (form_login) ;
  • une entitĂ© User persistĂ©e avec Doctrine ;
  • une chaĂ®ne d'envoi d'e-mails configurĂ©e (Symfony Mailer), puisque le code du second facteur part par e-mail.

Tout au long du tutoriel, remplacez <VOTRE_APP> par le nom de votre application et utilisez vos propres variables d'environnement pour les valeurs sensibles (adresse d'expéditeur, DSN mail, etc.) — ne codez jamais ces valeurs en dur.

Étape 1 — Installer le bundle

Installez le bundle et les deux briques dont nous avons besoin : la 2FA par e-mail et la reconnaissance des appareils de confiance.

composer require scheb/2fa-bundle scheb/2fa-email scheb/2fa-trusted-device

Étape 2 — Préparer l'entité User

Le bundle a besoin que votre utilisateur sache s'il a la 2FA activée, où envoyer le code et quel code est en cours. On implémente pour cela deux interfaces : celle de la 2FA e-mail et celle des appareils de confiance.

Point clé : le second facteur ne sera exigé que si l'utilisateur l'a activé. C'est le rôle du champ is2faEnabled, que renvoie isEmailAuthEnabled().

use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface;
use Scheb\TwoFactorBundle\Model\TrustedDeviceInterface;

class User implements UserInterface, PasswordAuthenticatedUserInterface,
                      TwoFactorInterface, TrustedDeviceInterface
{
    #[ORM\Column] private bool $is2faEnabled = false;
    #[ORM\Column(nullable: true)] private ?string $authCode = null;
    #[ORM\Column] private int $trustedVersion = 0;

    // --- 2FA e-mail ---
    public function isEmailAuthEnabled(): bool { return $this->is2faEnabled; }
    public function getEmailAuthRecipient(): string { return $this->email; }
    public function getEmailAuthCode(): ?string { return $this->authCode; }
    public function setEmailAuthCode(string $authCode): void { $this->authCode = $authCode; }

    // --- appareil de confiance ---
    public function getTrustedTokenVersion(): int { return $this->trustedVersion; }
}

Deux champs méritent une explication :

  • authCode stocke le code e-mail en cours de validitĂ© ; il est Ă©phĂ©mère et Ă©crasĂ© Ă  chaque nouvelle demande.
  • trustedVersion permet d'invalider d'un coup tous les appareils de confiance d'un utilisateur : il suffira plus tard d'incrĂ©menter ce nombre.

Générez et appliquez la migration Doctrine correspondante pour créer ces colonnes :

php bin/console make:migration
php bin/console doctrine:migrations:migrate

Étape 3 — Configurer le bundle

Créez (ou complétez) le fichier de configuration du bundle. On active l'e-mail et l'appareil de confiance.

# config/packages/scheb_2fa.yaml
scheb_two_factor:
    email:
        enabled: true
        sender_email: '%env(MAILER_SENDER)%'   # adresse d'expéditeur via variable d'env
        digits: 6
    trusted_device:
        enabled: true
        lifetime: 5184000    # durée de confiance en secondes (ici 60 jours)
        cookie_secure: true  # cookie transmis en HTTPS uniquement

Notez cookie_secure: true : le cookie d'appareil de confiance ne doit jamais transiter en clair. Et sender_email pointe vers une variable d'environnement — pas une adresse écrite en dur.

Étape 4 — Brancher le firewall

C'est ici que Symfony apprend à intercaler l'étape 2FA dans le processus de connexion. On déclare le formulaire de code dans le firewall, puis on rend le process 2FA joignable pendant que la connexion est « en attente ».

# config/packages/security.yaml
security:
    firewalls:
        main:
            # … votre authentification primaire (form_login) reste en place …
            two_factor:
                auth_form_path: 2fa_login        # route du formulaire de saisie du code
                check_path: 2fa_login_check      # cible du POST (gérée par le bundle)

    access_control:
        # le process 2FA doit ĂŞtre accessible PENDANT la connexion en attente
        - { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }

Déclarez ensuite les routes du bundle :

# config/routes/scheb_2fa.yaml
2fa_login:
    path: /2fa
    controller: "scheb_two_factor.form_controller::form"
2fa_login_check:
    path: /2fa_check

Vérification intermédiaire : donnez is2faEnabled = true à un compte de test, puis connectez-vous. Après l'e-mail + le mot de passe, l'utilisateur doit recevoir un code par e-mail et être redirigé vers /2fa pour le saisir avant d'accéder à l'application. Si c'est le cas, le cœur de la 2FA fonctionne.

Étape 5 — Laisser l'utilisateur activer / désactiver la 2FA

La 2FA est opt-in : chaque utilisateur choisit de l'activer. Ajoutez une route qui bascule is2faEnabled. On la protège par un jeton CSRF et on exige une session pleinement authentifiée (pas une simple 2FA en cours).

#[Route('/2fa/toggle', name: 'app_2fa_toggle', methods: ['POST'])]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
public function toggle(Request $request, EntityManagerInterface $em): Response
{
    if (!$this->isCsrfTokenValid('2fa_toggle', $request->request->get('_token'))) {
        throw $this->createAccessDeniedException();
    }
    $user = $this->getUser();
    $user->setIs2faEnabled(!$user->isEmailAuthEnabled());
    $em->flush();

    return $this->redirectToRoute('app_account');
}

Pourquoi IS_AUTHENTICATED_FULLY et non IS_AUTHENTICATED_2FA_IN_PROGRESS ? Parce qu'activer ou désactiver son second facteur est une action sensible : elle ne doit être possible qu'une fois la connexion complètement établie.

Étape 6 — Rendre le formulaire de code accessible

Le champ où l'utilisateur saisit son code est un formulaire comme un autre : il doit être accessible. Quelques règles simples suffisent à le rendre confortable, y compris sur mobile et pour les lecteurs d'écran :

  • un <label for> explicite (« Code de vĂ©rification reçu par e-mail »), et non un simple placeholder ;
  • inputmode="numeric" pour afficher le clavier numĂ©rique sur mobile ;
  • autocomplete="one-time-code" pour proposer le remplissage automatique du code ;
  • en cas de code refusĂ©, reliez le message d'erreur au champ via aria-describedby et ajoutez aria-invalid="true" ;
  • une case « Faire confiance Ă  cet appareil » avec un <label> clair : elle active le cookie de confiance configurĂ© Ă  l'Ă©tape 3 ;
  • un focus visible et aucune limite de temps cachĂ©e.

Étape 7 — Tester le parcours complet

Vérifiez les quatre cas suivants ; s'ils passent tous, votre 2FA est en place :

  1. 2FA activée (is2faEnabled = true) → la connexion exige le code e-mail avant d'accéder à l'application.
  2. Appareil de confiance (case cochée précédemment) → le code n'est pas redemandé sur ce même appareil.
  3. Code erroné → refus, avec un message d'erreur relié au champ.
  4. Révocation → incrémentez trustedVersion pour l'utilisateur : tous ses appareils de confiance redemandent le code à la prochaine connexion.

Félicitations : votre application exige désormais un second facteur, tout en respectant les appareils de confiance et le choix de l'utilisateur.

Aller plus loin — ajouter le TOTP (application d'authentification)

Une fois la 2FA e-mail maîtrisée, vous pouvez proposer le TOTP : un code généré par une application (Google Authenticator, Aegis, etc.), sans dépendre de l'e-mail.

Installez la brique dédiée :

composer require scheb/2fa-totp

Votre entité User implémentera alors l'interface TOTP du bundle et exposera un secret TOTP propre à chaque utilisateur (stocké dans une nouvelle colonne). Activez le TOTP dans scheb_2fa.yaml de la même manière que l'e-mail, et proposez à l'utilisateur un QR code d'enrôlement. Traitez le secret TOTP comme une donnée sensible : il ne doit jamais apparaître dans les logs ni transiter en clair.

Ressources

Assistant documentaire

Posez une question sur la documentation. Les réponses citent leurs sources — un clic ouvre le document à gauche.