Symfony 8.2, attendu en novembre 2026, permet de déconnecter un utilisateur uniquement par POST grâce aux helpers Twig logout_form() et logout_button(), de renvoyer une réponse JSON en mettant logout.target à null, et de synchroniser la déconnexion avec un fournisseur OpenID Connect via le back-channel logout. Le billet officiel du 13 octobre 2026 regroupe ces trois changements.
Ton lien de déconnexion est un <a href>. Un navigateur qui précharge la page, un bot qui génère un aperçu de lien, un scanner de sécurité ou un proxy d'entreprise peuvent le suivre sans que l'utilisateur ait cliqué. Résultat : des sessions qui sautent sans raison apparente, et un ticket de support impossible à reproduire. Le correctif existe depuis toujours, n'accepter que POST, mais presque personne ne l'applique : il fallait écrire un formulaire avec son jeton CSRF là où une ligne de Twig suffisait.
Pourquoi un logout en GET est un problème
HTTP définit GET comme une méthode sûre, sans effet de bord : n'importe quel intermédiaire a le droit de la rejouer. Un lien de déconnexion viole ce contrat, puisqu'il détruit la session. Le billet l'écrit sans détour : les navigateurs, les bots de prévisualisation, les scanners de sécurité et les proxies d'entreprise suivent les liens d'eux-mêmes. J'ajoute une analyse qui n'est pas dans le billet : une page tierce peut aussi forcer la déconnexion avec une simple image pointant vers /logout. C'est plus nuisible que dangereux, mais c'est exactement le genre de défaut qu'on traîne pendant des années. Pour le contexte plus large, notre article sur logout, CSRF et redirections dans Symfony couvre l'état de l'art avant 8.2.
Comment passer le logout en POST avec methods: [POST]
La parade consiste à restreindre la route de logout à POST :
# config/routes.yaml
app_logout:
path: /logout
methods: [POST]Point d'attention : rien ne change tant que tu ne déclares pas cette contrainte. D'après la PR #65129 de Nicolas Grekas, la route de logout enregistrée automatiquement accepte toujours GET, sans dépréciation. Mettre à jour Symfony ne suffit donc pas. Et un lien généré par logout_path() produit un GET que le routeur rejette avant que le listener de logout ne s'exécute : il faut remplacer chaque lien par un formulaire.
logout_form() ou logout_button() : lequel utiliser ?
logout_form() renvoie l'action et les champs cachés, jeton CSRF compris, et te laisse le contrôle du HTML :
{% set logout = logout_form() %}
<form method="post" action="{{ logout.action }}">
{% for name, value in logout.fields %}
<input type="hidden" name="{{ name }}" value="{{ value }}">
{% endfor %}
<button>Log out</button>
</form>Comme logout_path(), la fonction accepte le nom du firewall en argument optionnel ; côté PHP, l'équivalent est LogoutUrlGenerator::getLogoutForm(). La PR précise que le nom du paramètre CSRF et le jeton viennent bien de la configuration du firewall, ce qui évite un piège classique des formulaires écrits à la main. Si tu n'as pas besoin de personnaliser le markup, logout_button() rend le formulaire complet, protection CSRF stateless comprise :
{{ logout_button() }}
{# optional: the label, the firewall name, and the attributes of the button and the form #}
{{ logout_button('Sign out', attr: {class: 'btn-link'}, form_attr: {class: 'd-inline'}) }}Et l’impersonation en POST ?
Le problème est le même pour l'impersonation : un lien « Quitter l'utilisateur » en GET déclenche un changement d'identité sur un simple préchargement. Si tu restreins l'impersonation à POST, deux helpers rendent les formulaires correspondants. impersonation_exit_button() ne rend rien quand personne n'est impersonné, donc tu peux le laisser dans le layout sans condition :
{{ impersonation_button('Impersonate', user.userIdentifier, path('homepage')) }}
{{ impersonation_exit_button(target_uri: path('admin_users')) }}Comment renvoyer du JSON au logout, sans redirection
Avec json_login, tu construis la réponse de connexion dans un contrôleur, mais pour le logout il fallait jusqu'ici un listener d'événement. La PR #46320 de Jérôme Vasseur règle cette asymétrie : avec target à null, Symfony ne redirige plus après la déconnexion.
Votre équipe utilise Claude Code ?
Formation intra sur vos projets réels : de la demi-journée à 2 jours, 6 devs max.
# config/packages/security.yaml
security:
firewalls:
main:
json_login:
check_path: auth_login
logout:
path: auth_logout
target: ~La requête atteint alors le contrôleur de la route de logout, qui construit la réponse :
// src/Controller/AuthController.php
class AuthController extends AbstractController
{
#[Route('/login', name: 'auth_login')]
public function login(): JsonResponse
{
return $this->json($this->getUser());
}
#[Route('/logout', name: 'auth_logout')]
public function logout(): JsonResponse
{
return $this->json(null);
}
}Pour une SPA ou une API mobile, c'est le comportement attendu : un code HTTP et un corps JSON, pas une 302 vers une page HTML. Note que la route d'exemple ne restreint pas la méthode : si tu veux aussi le durcissement POST, ajoute methods: ['POST'] toi-même.
Comment synchroniser le logout avec un fournisseur OpenID Connect
Dernier cas : l'utilisateur se déconnecte chez le fournisseur d'identité, pas chez toi. Sans mécanisme dédié, sa session applicative reste ouverte. Florent Morselli (PR #66196) ajoute au login OpenID Connect natif le support de la spécification Back-Channel Logout : le fournisseur envoie, de serveur à serveur, un logout token signé, et Symfony déconnecte l'utilisateur à sa prochaine requête.
# config/packages/security.yaml
security:
firewalls:
main:
oidc_login:
# ...
backchannel_logout: true
# or: { path: /oidc/backchannel-logout, cache: cache.security, lifetime: 86400 }Symfony vérifie la signature du token contre les clés du fournisseur, puis ses claims, et enregistre la session terminée dans un pool de cache. Tu déclares ensuite l'URL de logout comme backchannel_logout_uri du client chez le fournisseur.
Les limites du back-channel logout en production
- Il ne fonctionne que sur les firewalls stateful ; l’option est refusée sur un firewall stateless.
- Le pool de cache doit être partagé par tous tes serveurs et ne doit pas évincer d’entrées avant leur expiration. Selon la PR, APCu ou un Redis en éviction LRU perdraient des déconnexions sans la moindre erreur.
- La durée de vie du cache doit couvrir la durée d’inactivité maximale d’une session ; au-delà, le navigateur retrouve un login fonctionnel.
- La session est dé-authentifiée, pas invalidée. Pour l’invalider comme un logout classique, écoute
TokenDeauthenticatedEvent. - Garde le chemin hors de tes règles
access_control: le fournisseur poste sans session ni cookie.
Comment migrer son application
- Recense chaque
logout_path()dans tes templates, menus, e-mails, pages d’erreur et back-offices, ainsi que les liens d’impersonation. - Remplace-les par
logout_button(), ou par un formulaire basé surlogout_form()si tu dois maîtriser le HTML. - Passe la route en
methods: [POST]seulement quand plus aucun lien GET ne subsiste : un lien oublié renverra une 405. - Ajoute trois tests fonctionnels, comme la PR #65129 : le formulaire déconnecte, un POST sans jeton est refusé, un
GET /logoutrenvoie 405. - Vérifie les clients non navigateur (SPA, mobile) qui appelaient
GET /logout.
Ce logout durci complète les autres renforcements de la version, comme le sudo mode et IS_AUTHENTICATED_RECENTLY.
Ma lecture
Le logout en GET n'a plus d'excuse. Il était déjà mauvais, mais le formulaire à écrire à la main servait de prétexte ; avec logout_button(), il ne reste plus d'argument. Sur toute nouvelle application 8.2, déclare methods: [POST] dès le premier jour. Les deux autres nouveautés sont des opt-in à adopter selon ton cas : target: ~ pour les API JSON, backchannel_logout si ta session doit suivre celle de ton fournisseur d'identité, après avoir vérifié ton pool de cache, car c'est là que ça casse en silence.
La migration des liens de logout est typiquement le chantier où un agent comme Claude Code rend le plus service : une règle simple à appliquer partout, et un test pour prouver qu'on n'a rien oublié. À condition que l'équipe sache le cadrer et relire ce qu'il produit. C'est le principe de la formation Claude Code en intra : apprendre à tes développeurs à le faire sur vos propres projets, de la demi-journée à deux jours, en petit groupe.

É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