Why must a custom Django authentication backend implement get_user(), and what does Django store in the session about the backend that logged a user in?
answer
- authenticate runs once, get_user every request
- _auth_user_backend
- still listed in the setting?
- primary key in, user or None out
basics
~20 slogin() stores the user's primary key and the backend's dotted path (_auth_user_backend) in the session; on every later request Django reloads that backend and calls its get_user(pk). A backend returning None there leaves the user anonymous.
solid answer
~40 s`authenticate()` runs once, at sign-in. After `login()`, the session holds `_auth_user_id`, `_auth_user_backend` (the dotted path from `user.backend`) and `_auth_user_hash`. On later requests `AuthenticationMiddleware` lazily resolves `request.user` by reading that path, checking it is **still in `AUTHENTICATION_BACKENDS`**, instantiating that backend and calling `get_user(user_id)` with the primary key. If `get_user()` returns `None` (the `BaseBackend` default) the user is anonymous, so a backend without it "logs in" and is logged out on the next click. `ModelBackend.get_user()` also returns `None` for inactive users. Consequences: removing or renaming a backend path logs out its sessions; reordering does not move existing sessions; and `login()` needs `backend=` when several backends exist and the user did not come from `authenticate()`.
code
python · 12 linesfrom django.contrib.auth import login
from accounts.forms import SignupForm
def signup(request):
form = SignupForm(request.POST or None)
if form.is_valid():
user = form.save()
# the user never went through authenticate(), so user.backend is unset
login(request, user, backend="django.contrib.auth.backends.ModelBackend")
...go deeper
Know that a backend needs both authenticate() and get_user(), and that the session remembers who you are by primary key.
Explain the three session keys, the reload path through AuthenticationMiddleware, and why BaseBackend's default get_user() causes instant logout.
Anticipate operational effects: renamed backend paths logging everyone out, deactivation timing, and passing backend= to login() after sign-up.
Use the stored path deliberately: retiring a backend is a revocation lever, and backend module paths become part of the session contract.
## Two methods, two moments A Django **authentication backend** has two required methods, and they run at different times: | Method | When it runs | Input | Output | |---|---|---|---| | `authenticate(request, **credentials)` | Once, when someone signs in | Whatever credentials the caller passes | A user or `None` | | `get_user(user_id)` | On requests after sign-in that touch `request.user` | The user's primary key from the session | A user or `None` | The first proves identity; the second **re-hydrates** it from the session cookie. The docs are explicit that `user_id` "has to be the primary key of your user object". ## What the session stores When `login(request, user)` succeeds it writes three keys into the session: - `_auth_user_id`: the primary key, serialised to a string. - `_auth_user_backend`: the dotted path of the backend, taken from `user.backend`, which `authenticate()` set on success. - `_auth_user_hash`: a hash derived from the password hash, used to invalidate sessions after a password change. The backend path is stored because different backends may know how to load different users. A directory backend's `get_user()` might add attributes that `ModelBackend` would not. ## How request.user is rebuilt `AuthenticationMiddleware` sets `request.user` to a lazy object. The first time it is used, `django.contrib.auth.get_user(request)` runs: 1. Read `_auth_user_id` and `_auth_user_backend` from the session; if either is missing, the user is anonymous. 2. Check that the stored path is **still listed** in `AUTHENTICATION_BACKENDS`. If it is not, the user is anonymous. 3. Import and instantiate that backend and call `backend.get_user(user_id)`. 4. If a user came back, verify `_auth_user_hash` against it; a hash that matches neither the current secret nor a `SECRET_KEY_FALLBACKS` entry flushes the session and the user is dropped. 5. Return the user, or an `AnonymousUser` when anything above produced `None`. ## What goes wrong without get_user() - **Instant logout.** `BaseBackend.get_user()` returns `None`. A backend that only implements `authenticate()` passes the login view, `login()` stores the session, and the very next request sees `AnonymousUser`. There is no error: it just "doesn't stay logged in". - **Wrong key type.** Implementing `get_user()` to look up by username instead of primary key fails, because Django passes the primary key. - **Silent active check.** `ModelBackend.get_user()` returns `None` when `user_can_authenticate()` fails, so deactivating a user takes effect on their next request. A custom `get_user()` that skips this check keeps deactivated users signed in until the session expires. The simplest fix for most custom backends is to subclass `ModelBackend` and inherit its `get_user()`. ## Operational consequences of the stored path 1. **Renaming or moving a backend class** changes its dotted path. Sessions created under the old path fail step 2 and every affected user is signed out. Plan such refactors or keep an alias module. 2. **Removing a backend** from the setting signs out everyone it authenticated, which is a feature when retiring a compromised source. 3. **Reordering** the list does not re-route existing sessions: each keeps the backend it logged in with. The docs advise clearing session data if you need everyone to re-authenticate by a new method (with the database session engine, `Session.objects.all().delete()`). 4. **Calling `login()` on a user that did not come from `authenticate()`**, for example right after sign-up, has no `user.backend`. With exactly one backend configured Django uses it; with several it raises `ValueError` unless you pass `backend="dotted.path"`. ## Async counterpart Since Django 5.2 backends may also define `aget_user()`, used by `request.auser()` in async code. `BaseBackend.aget_user()` wraps the sync method with `sync_to_async`, and `ModelBackend` ships a native `aget_user()` that performs the same active check. ## Debugging "I log in, then I'm logged out" This symptom almost always sits between the two methods. Check, in order: - Does the backend in `_auth_user_backend` define `get_user()`, or does it inherit the `BaseBackend` stub that returns `None`? - Does `get_user()` look up by **primary key**, the value actually stored in `_auth_user_id`? - Is the stored path spelled exactly as it appears in `AUTHENTICATION_BACKENDS`, after any refactor? - Does `get_user()` apply an active or verification check the user now fails? - Is the session itself surviving (cookie domain, session engine), which is a sessions question rather than a backend one? Inspecting the session in `manage.py shell` with the session engine's store answers the first three questions in a minute.
- Why does Django check that the stored backend path is still in AUTHENTICATION_BACKENDS instead of just importing it?So removing a backend from settings revokes it. If Django imported any stored path, a retired or compromised authentication source would keep vouching for existing sessions until they expired.
- A user was deactivated but is still browsing; which method decides when they lose access?The session's backend's `get_user()`. `ModelBackend.get_user()` applies `user_can_authenticate()`, so the next request that touches `request.user` becomes anonymous. A custom backend's `get_user()` must repeat that check, or deactivation waits for session expiry.
authenticate() is the passport check at the border; get_user() is the hotel reading your room number off the key card each time you come back. The card only works while the hotel still has a front desk under the name printed on it.
saying these in an interview costs you the question
- get_user() is only called once, right after authenticate().
- Django stores the whole user object in the session.
- Reordering AUTHENTICATION_BACKENDS re-routes users who are already logged in.
- get_user() receives the username the person typed at login.
- Renaming a backend class is a pure refactor with no effect on users.