Symfony 8.2 enregistre dans le token de sécurité les méthodes d'authentification prouvées pendant la session, avec le moment de leur dernier usage, selon les noms de la RFC 8176. Un voter peut alors exiger une méthode précise et déclencher une ré-authentification ciblée : c'est l'authentification step-up, qui consiste à demander une preuve plus forte, une clé matérielle par exemple, au moment d'une action sensible plutôt qu'à la connexion. Présentée le 12 octobre 2026 sur le blog officiel, contribution de Nicolas Grekas (PR #66065, #66069, #66242, #66184), la fonctionnalité arrive dans Symfony 8.2, attendu en novembre 2026.
Pour Symfony, un utilisateur connecté, c'est un token et des rôles. Le framework sait qui tu es, pas comment tu l'as prouvé. Un mot de passe saisi il y a trois secondes et une clé matérielle touchée à l'instant produisent le même ROLE_USER. Pour lire ses factures, c'est suffisant ; pour envoyer de l'argent ou changer un IBAN, c'est un angle mort. Jusqu'ici, on le comblait avec un flag en session ou un 2FA maison greffé sur chaque action sensible.
Pourquoi savoir comment l’utilisateur s’est authentifié ?
Le sudo mode de Symfony 8.2, présenté hier, répond à la question « quand l'utilisateur a-t-il saisi ses identifiants pour la dernière fois ? ». Cette nouveauté répond à la question complémentaire : avec quoi ? La PR #66065 explique pourquoi un timestamp unique ne suffit pas : il ne distingue pas « un second facteur au login il y a deux heures, puis un mot de passe il y a cinq minutes » de « deux mots de passe successifs ». Une politique de niveau d'assurance a besoin de cette distinction. La PR remplace donc le timestamp unique par une table qui associe chaque méthode au moment où elle a été prouvée.
Deux notions OpenID Connect reviennent dans la suite. Le claim amr liste les méthodes d'authentification utilisées ; le claim acr indique le niveau d'assurance atteint.
Enregistrer les méthodes d’authentification avec AuthenticationMethodBadge
Les noms viennent de la RFC 8176 et sont exposés en constantes de la classe AuthenticationMethod : pwd pour le mot de passe, hwk pour la clé matérielle. Côté natif, rien à faire : form_login, json_login et http_basic déclarent AuthenticationMethod::PASSWORD, et l'authenticator OpenID Connect lit le claim amr de l'ID token. Dans tes propres authenticators, tu ajoutes au passport un AuthenticationMethodBadge avec ce que tu as réellement vérifié :
// src/Security/SecurityKeyAuthenticator.php
namespace App\Security;
use Symfony\Component\Security\Core\Authentication\AuthenticationMethod;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\AuthenticationMethodBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
// ...
final class SecurityKeyAuthenticator extends AbstractAuthenticator
{
// ...
public function authenticate(Request $request): Passport
{
$assertion = $request->getPayload()->getString('assertion');
return new Passport(
new UserBadge($this->securityKeys->findUserIdentifier($assertion)),
new CustomCredentials($this->securityKeys->verify(...), $assertion),
[new AuthenticationMethodBadge(AuthenticationMethod::HARDWARE_KEY)],
);
}
}La mémoire est cumulative : un login par mot de passe puis une clé de sécurité donnent un token qui contient les deux. getAuthenticationProofs() renvoie les noms de méthode en clés et le dernier usage en valeurs, au format timestamp Unix :
$token->getAuthenticationProofs();
// ['pwd' => 1789140200, 'hwk' => 1789140260]Selon la PR #66069, le report des preuves d'un token à l'autre n'a lieu que pour le même utilisateur, et un login remember-me n'enregistre aucune preuve : présenter un cookie ne prouve rien sur les identifiants. login_link et access_token ne déclarent rien non plus. Garde aussi en tête que le badge est une assertion de ton authenticator, pas une preuve contrôlée par Symfony : le poser sans vérification réelle fabrique de la fausse assurance.
Exiger une clé de sécurité avec un voter et requestReAuthentication()
Jusqu'ici, un voter qui disait non produisait une 403. En 8.2, il peut appeler requestReAuthentication() sur l'objet Vote (PR #66242) : l'action protégée déclenche alors l'entry point de ré-authentification du firewall, celui du sudo mode. Exemple du billet :
// src/Security/Voter/SecurityKeyVoter.php
namespace App\Security\Voter;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\AuthenticationMethod;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class SecurityKeyVoter extends Voter
{
protected function supports(string $attribute, mixed $subject): bool
{
return 'IS_SECURITY_KEY_USED' === $attribute;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
{
if (isset($token->getAuthenticationProofs()[AuthenticationMethod::HARDWARE_KEY])) {
return true;
}
// users without a security key get a 403 error, because logging in again won't help them
$user = $token->getUser();
if ($user instanceof User && $user->hasSecurityKey()) {
$vote?->requestReAuthentication($attribute);
}
return false;
}
}Une action marquée #[IsGranted('IS_SECURITY_KEY_USED')] démarre alors la ré-authentification, mais seulement pour un utilisateur qui possède une clé qu'il n'a pas utilisée dans la session. Les autres reçoivent une 403. C'est la vraie leçon de conception : ne demande une ré-authentification que si elle peut guérir le refus. Ton entry point peut lire l'attribut de requête SecurityRequestAttributes::RE_AUTHENTICATION_ATTRIBUTE pour savoir quel attribut a été refusé, et afficher l'invite de clé plutôt que le formulaire de mot de passe.
Migration Symfony en vue ?
Bear Upgrade migre votre codebase 2,5× à 4,5× moins cher qu'une ESN, selon sa maturité.
Demander une authentification plus forte à un fournisseur OpenID Connect
Avec le login OpenID Connect natif, c'est le fournisseur qui ré-authentifie. Tu lui indiques le niveau attendu via le paramètre acr_values. Ce listener du billet ne l'ajoute que si ton voter a demandé la ré-authentification :
// src/EventListener/RequestSecurityKeyListener.php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Security\Http\Event\OidcAuthorizationRequestEvent;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
#[AsEventListener]
final class RequestSecurityKeyListener
{
public function __invoke(OidcAuthorizationRequestEvent $event): void
{
$deniedAttribute = $event->getRequest()->attributes->get(SecurityRequestAttributes::RE_AUTHENTICATION_ATTRIBUTE);
if ('IS_SECURITY_KEY_USED' === $deniedAttribute) {
// 'phrh' = phishing-resistant authentication with a hardware-protected key
$event->setParam('acr_values', 'phrh');
}
}
}Le piège central : acr_values est une demande, pas une garantie. Le fournisseur peut authentifier l'utilisateur autrement. Symfony 8.2 stocke donc le claim acr de l'ID token dans l'attribut de token oidc_acr, et c'est lui que ton voter doit vérifier :
if ($token->hasAttribute('oidc_acr') && 'phrh' === $token->getAttribute('oidc_acr')) {
return true;
}Les valeurs phr et phrh viennent de la spécification OpenID Connect EAP ACR Values, mais tu peux utiliser toute valeur supportée par ton fournisseur, à condition de vérifier celle qu'il renvoie réellement.
Pièges et bonnes pratiques
- Un seul attribut refusé. La ré-authentification ne se déclenche que si un seul attribut est refusé ; un contrôle combinant plusieurs rôles renvoie toujours une 403.
- Présence n’est pas fraîcheur. Le voter du billet teste la présence de
hwk, pas son âge : une clé touchée il y a trois heures passe encore. Pour un virement, compare le timestamp degetAuthenticationProofs()à une fenêtre que tu choisis. amrdépend du fournisseur. D’après la PR #66065, Okta et Auth0 l’envoient, Google non, et Keycloak demande un mapper. Sansamrexploitable, la preuve tombe sous une clé générique*: ta politique ne peut pas supposer quehwksera présent.- Table vide. Un token sans login interactif a une table de preuves vide, et
max([])lève une exception : garde explicite obligatoire si tu écris ta propre logique. - Tokens maison.
getAuthenticationProofs()et son setter sont annoncés en@methoden 8.2 et deviennent de vraies méthodes en 9.0. Un token custom qui ne les implémente pas n’a aucune preuve, donc ne satisfait jamais le sudo mode.
Un mot sur les exigences réglementaires : ni le billet ni les PR ne se réfèrent à un texte précis. La table de preuves fournit des données pour appliquer une politique interne ; elle ne rend pas conforme par elle-même. Pour le reste du durcissement de l'authentification, voir notre article sur logout, CSRF et remember me dans Symfony.
Ma lecture
Cette nouveauté ne rend pas ton application plus sûre toute seule. Elle te donne un vocabulaire standard (RFC 8176) et un point d'accroche propre, le voter, pour exprimer une politique que tu écrivais jusqu'ici en conditions éparses. Ma position : ne demande pas une méthode, demande une preuve récente de cette méthode. Un isset sur hwk vérifie un état de session, pas un geste récent. Avant la sortie de 8.2 :
- Liste les trois ou quatre actions où un simple mot de passe ne suffit pas : paiement, changement d’IBAN, export massif, droits d’administration.
- Ajoute
AuthenticationMethodBadgeaux authenticators maison qui vérifient un vrai facteur. - Écris un voter par exigence, avec
requestReAuthentication()uniquement quand se reconnecter peut résoudre le refus. - Avec un fournisseur d’identité, regarde ce qu’il renvoie réellement dans
amretacr, et contrôleoidc_acrcôté voter.
Si ton application embarque encore ses propres authenticators et voters pour les actions sensibles, la montée vers 8.2 est l'occasion de trier ce qui devient natif et ce qui doit rester sur mesure. C'est le genre de passage que Bear Upgrade prend en charge : Rector pour la mécanique, des tests autour des chemins d'authentification, et des Merge Requests prêtes à merger plutôt qu'un gros lot opaque.

Écrit par
Flavien Métivier
Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.
Mon parcours