skip to content

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