skip to content

Login & Logout Flow

authenticate() checks credentials, login() stores the user in the session and rotates its key, and logout() flushes it. Interviewers probe session fixation and logouts after a password change.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Django's contrib.auth, what is the difference between authenticate() and login(), and why does a sign-in view call both?

level: juniorimportance: must knowfreq 76%

answer

  1. checking versus remembering
  2. one returns a user or nothing
  3. the other writes to the session
  4. which backend vouched for the user

basics

~20 s

authenticate() checks credentials against the configured backends and returns a user or None without changing any state. login() takes that user and records it in the session, so later requests arrive with request.user set. A sign-in view needs both steps.

solid answer

~40 s

`authenticate(request, **credentials)` walks `AUTHENTICATION_BACKENDS`, asks each compatible backend to verify the credentials, and returns the first user it gets back, annotated with `user.backend`, or `None` after sending `user_login_failed`. It touches no session and sets no cookie. `login(request, user)` is the stateful step: it rotates or flushes the session, stores the user's primary key, the backend path and a password-derived hash in `request.session`, sets `request.user`, rotates the CSRF token and sends `user_logged_in`. So a view that only authenticates has proved the password but logs nobody in, and a view that calls `login()` on an unverified user object skips the credential check entirely. The built-in `LoginView` does both through `AuthenticationForm`.

code

python · 17 lines
python
from django.contrib.auth import authenticate, login
from django.shortcuts import redirect, render


def member_sign_in(request):
    error = None
    if request.method == 'POST':
        user = authenticate(
            request,
            username=request.POST.get('membership_number'),
            password=request.POST.get('password'),
        )
        if user is not None:
            login(request, user)
            return redirect('class-schedule')
        error = 'Membership number or password is incorrect.'
    return render(request, 'members/sign_in.html', {'error': error})

go deeper

for a junior

Recall the pairing: authenticate() returns a user or None, login() stores that user in the session, and request.user is an AnonymousUser when nobody is signed in.

for a middle

Explain what login() writes to the session, why it needs the backend path, and how AuthenticationMiddleware rebuilds request.user from those keys on the next request.

for a senior

Spot the bypass of calling login() on an unverified user and the ValueError with multiple backends, and know where the is_active check really lives.

for a principal

Use the split deliberately: re-verify credentials for sensitive actions with authenticate() alone, and let recovery flows call login() only after an equivalent proof.

## Two steps, two jobs `django.contrib.auth` splits signing in into **verification** and **persistence**: | Function | Input | Output | Side effects | |---|---|---|---| | `authenticate(request, **credentials)` | credentials such as `username` and `password` | a user object or `None` | none on success; sends `user_login_failed` on failure | | `login(request, user, backend=None)` | a user object already verified | nothing | writes the session, sets `request.user`, rotates the CSRF token, sends `user_logged_in` | Keeping them apart lets you verify a password without logging anyone in (for example to confirm a sensitive action) and lets flows such as password reset log a user in without asking for credentials again. ## What authenticate() does 1. It iterates over `AUTHENTICATION_BACKENDS` in order, skipping any backend whose `authenticate()` signature cannot accept the keyword arguments you passed. 2. Each backend returns a user or `None`. The first user wins; Django sets `user.backend` to that backend's dotted path. 3. A backend may raise `PermissionDenied` to veto the login outright; `authenticate()` catches it, stops trying other backends and falls through to failure. 4. On failure it sends `user_login_failed` with the credentials cleansed of anything that looks like a password or token, and returns `None`. With the default `ModelBackend`, an account whose `is_active` is `False` is rejected here too, so `authenticate()` returns `None` for a deactivated member. ## What login() does `login()` assumes the user is genuine. It: - rotates the session key for an anonymous session with `cycle_key()`, or empties it with `flush()` if it belonged to a different user; - stores three keys: `_auth_user_id` (the primary key as a string), `_auth_user_backend` (the backend path) and `_auth_user_hash` (an HMAC of the password field); - sets `request.user` for the rest of the current request; - rotates the CSRF token and sends `user_logged_in`. The backend path matters. `login()` takes it from the `backend` argument or from `user.backend`, which `authenticate()` set. If neither exists and more than one backend is configured, it raises `ValueError` asking you to pass `backend`. With a single backend it uses that one. On every later request, `AuthenticationMiddleware` makes `request.user` a lazy object that reads those keys, asks the stored backend's `get_user()` for the user and checks the hash; if anything is missing or wrong you get an `AnonymousUser`. ## Putting them together A members-only gym portal's sign-in view verifies first and persists second: ```python user = authenticate(request, username=number, password=password) if user is not None: login(request, user) ``` Most projects never write this by hand: `LoginView` uses `AuthenticationForm`, whose `clean()` calls `authenticate()` and then `confirm_login_allowed()` (which rejects inactive users with an error message), and whose `form_valid()` calls `login()` with `form.get_user()`. ## What the next request sees The split also explains what happens after the response is sent. On the following request `AuthenticationMiddleware` installs a lazy `request.user`; the first access runs `get_user()`, which: - reads `_auth_user_id` and `_auth_user_backend` from the session; - checks that the backend path is still listed in `AUTHENTICATION_BACKENDS`; - calls that backend's `get_user(user_id)`, which for `ModelBackend` also rejects inactive accounts; - compares the stored `_auth_user_hash` with a freshly computed one. Any failure yields `AnonymousUser`, whose `is_authenticated` is `False`, whose `pk` is `None` and whose permission checks come back empty. That is why templates can test `user.is_authenticated` without first checking for `None`. ## Timing and enumeration `ModelBackend.authenticate()` runs the password hasher even when no account matches the username, by hashing the submitted password on a throwaway user instance. The response time is then similar for an unknown membership number and a wrong password, which makes it harder to discover which numbers exist. Custom backends should keep that property. ## Common mistakes - **Calling only `authenticate()`** and expecting the member to be logged in on the next page. Nothing was stored, so the next request sees `AnonymousUser`. - **Calling `login()` on a user fetched by primary key** without verifying anything, which is an authentication bypass if the key came from the request. - **Treating `None` as an exception path.** `authenticate()` does not raise for bad credentials; you must test for `None`. - **Assuming `login()` checks `is_active`.** It does not; the check lives in `ModelBackend` and in `AuthenticationForm`.

  • Why can login() fail with a ValueError when you pass it a user loaded with get()?
    `login()` needs to store which backend vouched for the user. It reads `user.backend`, which only `authenticate()` sets. With several entries in `AUTHENTICATION_BACKENDS` and no `backend=` argument, it cannot choose and raises `ValueError`. Pass `backend='django.contrib.auth.backends.ModelBackend'` explicitly when logging in a user you verified another way, for example right after sign-up.
  • Does login() keep data the visitor put in the session before signing in?
    For an anonymous session, yes: `login()` calls `cycle_key()`, which issues a new session key but keeps the stored data, so a class the visitor picked before signing in survives. If the session already belonged to a different authenticated user, `login()` calls `flush()` instead and the data is discarded.
  • What does request.user return when nobody is logged in?
    An `AnonymousUser` instance, not `None`. It has `is_authenticated` set to `False`, `is_anonymous` set to `True`, `pk` set to `None`, and permission checks that return `False` or empty sets, so templates and views can use the same attributes without checking for `None` first.

saying these in an interview costs you the question

  • authenticate() logs the user in and sets the session cookie
  • authenticate() raises an exception when the password is wrong
  • login() re-checks the password before storing the user
  • login() refuses inactive users on its own
  • request.user is None for visitors who are not signed in
open as a page

Why does a plain link to Django's LogoutView stop working in Django 5.0 and later, and what does logout() actually clear?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Since 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.

open as a page

What does Django's login() do to the session it finds, and how does that protect a members-only portal against session fixation?

level: middleimportance: should knowfreq 52%

basics

~20 s

login() cycles the key of an anonymous session, keeping its data, and flushes a session that belonged to another user. A key planted before sign-in therefore never becomes authenticated. It then stores the user id, backend and password hash.

open as a page

A Django gym portal's custom password-change view logs the member out right after saving; why, and how does update_session_auth_hash() change what happens to their sessions?

level: seniorimportance: should knowfreq 44%

basics

~20 s

login() stores an HMAC of the password field in the session, and each request recomputes and compares it. A new password changes it, so the session is flushed. update_session_auth_hash() stores the new hash and cycles the key for the current session only.

open as a page

In an async Django view, why should you use await request.auser() and alogin() instead of request.user and login()?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

request.user is a lazy object whose first access reads the session and queries the user synchronously, which Django forbids inside an event loop. request.auser(), alogin(), alogout() and aauthenticate(), added in Django 5.0, do the same work asynchronously.

open as a page