03-comment-je-travaille/tutos/2fa-symfony-scheb.md

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 champ totpSecret est réservé sur l'entité User mais 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; }
}

authCode stocke le code e-mail en cours ; il est éphémère. trustedVersion permet 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 le trusted_parameter_name du 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 (trustedVersion courant) → 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

Assistant documentaire

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

Loading…
Loading the web debug toolbar…
Attempt #