Tuto — 2FA en Symfony avec scheb (e-mail + appareil de confiance)
Public visé : développeur back qui ajoute la double authentification (2FA) à une app
Symfony (cf. specs/auth-compte.md).
Objectif : exiger un second facteur Ă la connexion, sans harceler l'utilisateur sur
ses appareils de confiance, avec un formulaire accessible RGAA AA.
Référence transverse : Accessibilité — interface utilisateur.
Ce qui est en place (et ce qui est prévu)
codexia utilise scheb/2fa-bundle avec deux briques actives :
- 2FA par e-mail (
scheb/2fa-email) : un code à usage unique envoyé par e-mail. - Appareil de confiance (
scheb/2fa-trusted-device) : ne pas redemander le code sur un appareil déjà validé.
TOTP (
scheb/2fa-totp) est prévu, pas encore actif : le champtotpSecretest réservé sur l'entitéUsermais le bundle n'est pas installé. La section TOTP montre comment l'ajouter le moment venu — ne pas le présenter comme livré.
Étape 1 — Installer le bundle
composer require scheb/2fa-bundle scheb/2fa-email scheb/2fa-trusted-device
Étape 2 — Préparer l'entité User
L'utilisateur implémente l'interface e-mail de scheb et celle des appareils de confiance.
Le second facteur n'est exigé que si l'utilisateur l'a activé (is2faEnabled) — c'est le
rĂ´le de 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; } }
authCodestocke le code e-mail en cours ; il est éphémère.trustedVersionpermet d'invalider d'un coup tous les appareils de confiance (incrémenter la version).
Étape 3 — Configurer le bundle
# config/packages/scheb_2fa.yaml scheb_two_factor: email: enabled: true sender_email: no-reply@telaria.fr # passe par la chaîne mail (cf. guides/nouveau-domaine.md) digits: 6 trusted_device: enabled: true lifetime: 5184000 # 60 jours cookie_secure: true # HTTPS uniquement
Étape 4 — Brancher le firewall
# config/packages/security.yaml security: firewalls: main: # … authentification primaire (form_login) … two_factor: auth_form_path: 2fa_login # route du formulaire de code check_path: 2fa_login_check # cible du POST (gérée par le bundle) access_control: # le process 2FA doit être joignable PENDANT la connexion en attente - { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS } - { path: ^/admin, role: ROLE_ADMIN }
# config/routes/scheb_2fa.yaml 2fa_login: path: /2fa controller: "scheb_two_factor.form_controller::form" 2fa_login_check: path: /2fa_check
À ce stade, un utilisateur avec is2faEnabled = true reçoit, après l'e-mail + mot de
passe, un code par e-mail à saisir avant d'accéder à l'application.
Étape 5 — Laisser l'utilisateur activer/désactiver
La 2FA est opt-in : une route bascule is2faEnabled (cf. auth-compte.md §6,
route /2fa/toggle).
#[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'); }
Protéger la bascule par CSRF et l'exiger en session pleinement authentifiée (
IS_AUTHENTICATED_FULLY), pas en 2FA en cours.
Étape 6 — Accessibilité du formulaire de code (RGAA AA)
Le champ de saisie du code est un formulaire comme un autre — il doit être accessible
(auth-compte.md §10) :
<label for>explicite (« Code de vérification reçu par e-mail »), pas un simple placeholder.inputmode="numeric"+autocomplete="one-time-code": clavier numérique mobile et remplissage automatique du code.- Erreur reliée au champ par
aria-describedby;aria-invalid="true"si code refusé. - Case « faire confiance à cet appareil » avec un
<label>clair (active le cookie de confiance via letrusted_parameter_namedu bundle). - Focus visible, pas de limite de temps cachée. Voir Champs de formulaire.
Tester
Reprend les cas de auth-compte.md §11 :
is2faEnabled = true→ la connexion exige le code e-mail avant l'accès.- Appareil de confiance (
trustedVersioncourant) → le code n'est pas redemandé. - Code erroné → refus + message d'erreur relié au champ.
- Incrémenter
trustedVersion→ tous les appareils de confiance redemandent le code.
Extension TOTP (prévu)
Quand on activera l'authentification par application (Google Authenticator, etc.) :
composer require scheb/2fa-totp
User implĂ©mentera en plus TwoFactorInterface (variante TOTP) en exposant le secret dĂ©jĂ
réservé (totpSecret). Tant que ce n'est pas fait, ne pas documenter le TOTP comme
disponible (cf. auth-compte.md §13.1).
Pour aller plus loin
- Spécification auth/compte/2FA :
specs/auth-compte.md - Chaîne e-mail (envoi du code) :
guides/nouveau-domaine.md - Accès API par token (tuto compagnon, à produire) :
specs/auth-compte.md scheb/2fa-bundle: https://symfony.com/bundles/SchebTwoFactorBundle/current/index.html