Aller au contenu
symfonysecuriteoauthretention

Le bug d'authentification que le développeur ne peut pas voir

Sur une de mes applications, 378 des 533 inscrits se connectaient par Google. Tous étaient déconnectés toutes les trente minutes, depuis le premier jour. Personne ne l'avait signalé, et surtout : je ne pouvais pas le voir, parce que mon propre compte de test n'était pas concerné. Voici comment un bug pareil survit des mois, et par quel bout on le trouve.

Le symptôme, et il n'est jamais arrivé sous forme de bug

Le 29 juillet 2026, je remonte la piste d'une suppression de groupe sur FairChore, une application de partage de tâches domestiques. Je ne cherche pas un bug d'authentification. Je lis un journal, pas à pas, pour comprendre ce qu'une utilisatrice a fait.

Et je vois ceci : elle est sur une page, elle clique, et l'application la renvoie vers l'écran de connexion. Elle se reconnecte par Google. Elle reprend où elle en était. Le même motif revient plusieurs fois dans la journée.

Ce n'est pas une suppression accidentelle. C'est quelqu'un qui se fait éjecter de son compte et qui, à chaque fois, recommence sans se plaindre.

Pourquoi ce bug est invisible pour celui qui a écrit le code

Trois raisons se cumulent, et c'est leur cumul qui le rend indétectable.

Le développeur teste avec un compte mot de passe. Le formulaire de connexion classique de Symfony n'a pas le problème. La connexion Google, si. Quand on développe, on tape son identifiant et son mot de passe cent fois par jour, parce que c'est plus rapide que de passer par un fournisseur d'identité. On ne visite donc jamais le chemin cassé.

La configuration dit que tout va bien. Dans le security.yaml, on lit :

remember_me:
    secret: '%kernel.secret%'
    lifetime: 31536000 # 1 an
    always_remember_me: true

Un an de mémorisation, activée pour tout le monde, sans case à cocher. Relire ce fichier en cherchant la cause du problème donne exactement l'inverse de la réponse : il confirme que la fonctionnalité est demandée.

L'échec ne produit aucune trace. C'est le point qui compte, et il se lit dans le code de Symfony lui-même. Le composant qui décide d'activer la mémorisation est CheckRememberMeConditionsListener, et il commence par ceci (Symfony 8.0.14) :

$passport = $event->getPassport();
if (!$passport->hasBadge(RememberMeBadge::class)) {
    return;
}

Un return nu. Pas d'exception, pas d'avertissement, pas même une ligne en journal de débogage : quand le badge manque, Symfony sort sans un mot. Plus bas dans la même méthode, un autre cas d'abandon, lui, écrit bien un message de débogage. Celui-là, non.

Autrement dit : la seule branche qui ne dit rien est celle où la fonctionnalité n'a jamais été branchée.

Ce qui se passait vraiment, en deux mécanismes qui n'ont rien à voir

Premier mécanisme : la session serveur meurt vite. Sur le serveur, les fichiers de session PHP sont purgés par une tâche planifiée qui tourne toutes les trente minutes et supprime les fichiers plus vieux que la durée de vie configurée. C'est du ménage d'hébergement standard, indépendant de l'application. Une session inactive ne survit donc pas à la nuit, ni souvent à une pause déjeuner.

Second mécanisme : le cookie qui devrait prendre le relais n'a jamais existé. L'authentificateur Google renvoyait un passeport sans badge :

return new SelfValidatingPassport(
    new UserBadge($email, function () { /* … */ })
);

SelfValidatingPassport est le bon outil : quand l'identité est établie par un tiers de confiance, il n'y a pas de mot de passe à vérifier. Mais un passeport ne transporte que ce qu'on lui donne. Sans RememberMeBadge, le listener ci-dessus sort par son return nu, et aucun cookie de mémorisation n'est jamais posé.

Le formulaire de connexion, lui, n'a pas ce problème : form_login pose le badge tout seul. Le bug ne frappe donc que les authentificateurs écrits à la main, ce qui veut dire : OAuth, SSO d'entreprise, lien magique par e-mail. C'est-à-dire, précisément, les chemins par lesquels arrivent la plupart des utilisateurs d'une application grand public.

Le correctif tient en deux lignes

return new SelfValidatingPassport(
    new UserBadge($email, function () { /* … */ }),
    [new RememberMeBadge()]
);

Un use en haut du fichier, un tableau de badges en second argument. Le commit fait quatre lignes de code applicatif, plus un test unitaire qui vérifie que le passeport porte bien le badge, pour que personne ne le retire par inadvertance dans six mois.

Ce que le correctif ne fait pas, et il faut le dire aux gens qui vous paient : il n'agit qu'à la prochaine connexion de chacun. Les utilisateurs déjà là ne récupèrent rien rétroactivement ; ils cessent d'être éjectés à partir du jour où ils se reconnectent.

Ce que je retiens, et ce que ça dit d'un audit

Un bug qui déconnecte 71 % des inscrits plusieurs fois par jour, dans une application installable sur l'écran d'accueil, n'est pas un défaut d'ergonomie. C'est une fuite de rétention : l'écran de connexion qui resurgit à chaque ouverture est l'une des façons les plus efficaces de faire abandonner une application, et elle ne remonte jamais sous forme de ticket. Les gens ne signalent pas « je dois me reconnecter », ils arrêtent de venir.

Trois leçons que j'applique depuis à tous mes états des lieux de code PHP :

  1. Chercher les chemins qu'on ne prend jamais. Le chemin dominant des utilisateurs n'est presque jamais celui du développeur. Sur cette application, le journal comptait 92 passages par la connexion Google contre 19 par le formulaire, pour des populations actives comparables.
  2. Se méfier de la configuration qui rassure. always_remember_me: true était vrai, et ne servait à rien. Une option activée n'est pas une fonctionnalité qui marche : entre les deux, il y a du code, et c'est lui qu'il faut lire.
  3. Traiter le silence comme un signal. Un return sans journal est un endroit où votre application peut échouer sans jamais vous le dire. Ce sont ces endroits-là qu'il faut inventorier en premier, pas les exceptions : celles-là, au moins, on les voit.

Et une quatrième, qui n'est pas technique : ce bug a été trouvé en lisant le parcours d'une personne réelle, pas en relisant du code. Les journaux d'une application racontent ce que vivent vos utilisateurs, et personne ne les lit.

Audit de code

Dites-moi quelle décision vous attend

Décrivez l'application en quelques lignes : ce qu'elle fait, depuis quand elle existe, qui l'a écrite, et la décision que vous avez à prendre. Je réponds sous 24 h, en vous disant franchement si un audit est utile dans votre cas, ou si la réponse tient en un échange.