Why does a plain link to Django's LogoutView stop working in Django 5.0 and later, and what does logout() actually clear?
answer
- an image tag could sign you out
- allowed HTTP methods on the view
- deprecated one release, removed later
- signal first, then the session goes
basics
~20 sSince Django 5.0 LogoutView accepts only POST (and OPTIONS), so a GET link gets 405 and logs nobody out. logout() sends user_logged_out, flushes the current session, deleting its data and key, and sets request.user to AnonymousUser.
solid answer
~40 sLogging out with `GET` meant any page could sign a member out with an `<img>` or a prefetched link, a cross-site request forgery that needs no token. Django deprecated `GET` logout in 4.1 and removed it in 5.0: `LogoutView` now has `http_method_names = ['post', 'options']`, so a link returns `405 Method Not Allowed`. The fix is a small `<form method="post">` with `{% csrf_token %}` and a submit button. `logout(request)` itself sends `user_logged_out` first, so receivers still know who is leaving, then calls `request.session.flush()`, which deletes the stored record and all its data and drops the key, and finally sets `request.user` to `AnonymousUser`. It ends only this browser's session; other devices stay signed in.
code
python · 10 linesfrom django.contrib.auth import views as auth_views
from django.urls import path
urlpatterns = [
path('sign-in/', auth_views.LoginView.as_view(), name='login'),
path('sign-out/', auth_views.LogoutView.as_view(next_page='gym-home'), name='logout'),
]
# settings.py alternative to next_page:
# LOGOUT_REDIRECT_URL = 'gym-home'go deeper
Recall that logout in current Django is a POST form with a CSRF token, and that logout() leaves request.user as an AnonymousUser.
Explain the order inside logout(): signal first, then flush(), then AnonymousUser, and what flush() removes compared with cycle_key().
Handle the upgrade: find GET logout links before moving to 5.0, and explain why logout() does not sign out other devices and what does.
Treat logout scope as a product decision: this-device logout, all-devices logout and forced logout after an incident need different mechanisms and should be designed together.
## Why GET logout was removed A `GET` request is supposed to be safe: fetching a URL should not change state. Logging out changes state, and when Django's `LogoutView` accepted `GET`, any third-party page could log a member out by embedding the logout URL in an `<img>` tag, and browsers that prefetch links could do it by accident. It is a nuisance attack rather than a data leak, but it is still a forged request. Django closed it in two releases: | Release | Change | |---|---| | 4.1 | logging out via `GET` in `LogoutView` deprecated | | 5.0 | support for `GET` removed from `LogoutView` and `logout_then_login()` | | 6.1 (current) | `LogoutView.http_method_names` is `['post', 'options']` | A `GET` to the view now falls through to Django's generic method check and returns **405 Method Not Allowed**. Old templates with `<a href="{% url 'logout' %}">` stop working after an upgrade, which is why this shows up in interviews. ## The replacement markup A gym portal's navigation bar needs a form instead of a link: ```html <form method="post" action="{% url 'logout' %}"> {% csrf_token %} <button type="submit">Sign out</button> </form> ``` The button can be styled as a link. The POST passes through the normal CSRF check, so another site can no longer trigger it. ## What logout() does, step by step `django.contrib.auth.logout(request)` is what the view calls, and you can call it from your own views: 1. It reads `request.user`; if that is not authenticated it treats the user as `None`. 2. It sends **`user_logged_out`** with the user, *before* anything is cleared, so receivers can record who left. 3. It calls **`request.session.flush()`**: the session data is cleared, the stored record is deleted, and the key is dropped so a new one is created only if the response stores something again. 4. It sets `request.user` (and `request.auser`) to an **`AnonymousUser`**. Because `flush()` wipes everything, any non-auth data in the session, such as a half-finished booking, is gone too. That is intended: the next person at a shared kiosk must not inherit it. ## What logout() does not do - It does **not** end the member's other sessions. Each browser or phone has its own session record; `logout()` deletes only the one on this request. - It does **not** touch the password or the session hash, so other devices remain valid. - It does **not** redirect by itself; the view decides where to go. To sign a member out everywhere, change something the other sessions depend on (a password change invalidates their session hash) or delete their session records. ## Where the view sends the user `LogoutView` redirects to, in order of preference: a safe `next` value from the request, its own `next_page` attribute, or `LOGOUT_REDIRECT_URL`. The setting defaults to `None`; with nothing configured the view typically renders `registration/logged_out.html` at the logout URL itself. Checking that `next` is safe is the same host check the login redirect uses. ## Testing the upgrade When a project moves to 5.0 or later, a few quick checks catch the breakage before members do: - search templates for links whose target is the logout URL name and convert each one to a form; - in tests, assert that `client.get(reverse('logout'))` returns 405 and that `client.post(reverse('logout'))` leaves `client.session` without `_auth_user_id`; - check any JavaScript that logged out with a `fetch()` using the default `GET` method, and switch it to `POST` with the CSRF header. ## Async views `alogout(request)`, added in 5.0, does the same work with `await request.auser()`, `user_logged_out.asend()` and `request.session.aflush()`.
- Why is user_logged_out sent before the session is flushed?Receivers usually want to know who left, for an audit log or to clear a per-user cache. After `flush()` and the switch to `AnonymousUser` that identity is gone from the request, so `logout()` sends the signal first with the real user object. For an anonymous visitor the signal still fires, with `user=None`.
- How would you log a member out of every device in Django?`logout()` ends only the current session. Changing the password invalidates every other session on its next request, because the stored `_auth_user_hash` no longer matches. Without a password change you must delete that member's session records, which with the database backend means finding sessions whose decoded data carries their `_auth_user_id`.
saying these in an interview costs you the question
- A GET link to LogoutView still logs the user out in Django 6.1
- logout() removes only _auth_user_id and keeps other session data
- logout() ends the member's sessions on every device
- user_logged_out fires after the session is flushed
- LogoutView redirects to LOGIN_URL by default