Aller au contenu principal
Retour au blog

Logout Symfony 8.2 : POST-only, logout_button() et back-channel

Symfony 8.2 sécurise le logout : formulaire POST, helper Twig logout_button(), réponse JSON sans redirection et back-channel logout OpenID Connect.

Flavien Métivier · 11 min de lecture
Sommaire · 9 parties

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 :

yaml
# 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 :

bash
{% 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 :

bash
{{ 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 :

bash
{{ 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.

Voir les formats →
yaml
# 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 :

php
// 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.

yaml
# 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é sur logout_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 /logout renvoie 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.

Cet article vous a été utile ? Partagez-le.

Portrait de Flavien Métivier, fondateur de The Bearded Bear : barbe longue, lunettes, sur fond sombre

É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

À lire ensuite

Votre équipe utilise Claude Code ?

Formation intra sur vos projets réels : de la demi-journée à 2 jours, 6 devs max.

Newsletter Bear Bytes

Veille Claude Code, Symfony et PHP, retours d'expérience terrain et conseils techniques, directement dans votre boîte mail.