Symfony 8.2 makes it easier to prevent accidental logouts, return custom logout responses and synchronize logout with an OpenID Connect provider.

Logging Out with a POST Form

Nicolas Grekas
Contributed by Nicolas Grekas in #65129 , #65124 and #66786

Logging out with a GET request only takes a link, and browsers, link-preview bots, security scanners and corporate proxies follow links on their own. The fix is to only accept POST requests on your logout route:

1
2
3
4
# config/routes.yaml
app_logout:
    path: /logout
    methods: [POST]

This requires a form instead of a link. To help build it, Symfony 8.2 adds a new logout_form() Twig function that returns the form action and its hidden fields (including the CSRF token, if enabled in the firewall):

1
2
3
4
5
6
7
8
9
{% 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>

Like logout_path(), it accepts the firewall name as an optional argument. In PHP code, use the new LogoutUrlGenerator::getLogoutForm() method.

Since every application ends up writing that same form, Symfony 8.2 also adds a logout_button() Twig function that renders it for you. Its CSRF field works like the one of Symfony forms, including with stateless CSRF protection:

1
2
3
4
{{ 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'}) }}

The same goes for impersonation when you restrict it to POST requests: the new impersonation_button() and impersonation_exit_button() functions render the forms of impersonation_form() and impersonation_exit_form(). The exit button renders nothing when the user is not impersonating anyone:

1
2
{{ impersonation_button('Impersonate', user.userIdentifier, path('homepage')) }}
{{ impersonation_exit_button(target_uri: path('admin_users')) }}

Disabling the Redirect After Logout

Jérôme Vasseur
Contributed by Jérôme Vasseur in #46320

When using JSON login, you can customize the login response in a controller, but customizing the logout response requires an event listener. In Symfony 8.2, set the logout.target option to null to disable the redirect after logout:

1
2
3
4
5
6
7
8
9
# config/packages/security.yaml
security:
    firewalls:
        main:
            json_login:
                check_path: auth_login
            logout:
                path: auth_logout
                target: ~

After logging the user out, the request now reaches the controller of the logout route, so you can create the response there:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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);
    }
}

OpenID Connect Back-Channel Logout

Florent Morselli
Contributed by Florent Morselli in #66196

The OpenID Connect login feature added in Symfony 8.2 now supports the OpenID Connect Back-Channel Logout specification: when users log out from the identity provider, the provider sends a signed logout token to your application and Symfony logs those users out on their next request.

1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    firewalls:
        main:
            oidc_login:
                # ...
                backchannel_logout: true
                # or: { path: /oidc/backchannel-logout, cache: cache.security, lifetime: 86400 }

Symfony verifies the signature and claims of the logout token and stores the ended session in a cache pool. Then, register the logout URL as the backchannel_logout_uri of your client in the identity provider. Keep in mind that this only works in stateful firewalls and that the cache pool must be shared by all your servers and must not evict items before they expire.

Published in #Living on the edge