Django

Sessions and cookies — how Django remembers a logged-in user

HTTP itself has no memory between requests. The auth lesson took request.user for granted — this is the actual mechanism underneath it, and why it depends on a plain cookie doing exactly one job well.

Intermediate

4 min read

The problem: HTTP forgets everything between requests

Every HTTP request (from the system design domain's HTTP/APIs lesson) is independent — the server has no built-in memory of any previous request from the same browser. Without something bridging that gap, there'd be no way to say "this request and that earlier request came from the same logged-in user" at all; each request would look identical to the server whether it came from a logged-in Ada or a stranger who'd never logged in.

Server response includes:
Set-Cookie: sessionid=8f14e45fceea167a5a36dedd4bad3195

Every later request from that same browser includes:
Cookie: sessionid=8f14e45fceea167a5a36dedd4bad3195

A cookie is a small piece of data the server asks the browser to store, which the browser then automatically attaches to every subsequent request to that same domain. This is the actual bridge across HTTP's request-to-request amnesia: the cookie itself doesn't have to contain the user's data — it just has to contain something unique enough to identify which stored session belongs to this browser, on the server's side.

request.session["cart_items"] = [1, 2, 3]
request.session["last_visited"] = "products"

Django's session framework stores session data server-side — by default, in a database table (django_session) — and the cookie (sessionid, set by SessionMiddleware from the middleware lesson) holds only a random, unguessable key pointing at that row, never the actual data. request.session behaves like a dict, and reading/writing to it transparently reads and writes the corresponding database row behind the scenes. This server-side storage is deliberate: since the cookie only holds a reference, not the actual data, there's nothing sensitive for someone intercepting or inspecting the cookie itself to actually read.

The chain that makes request.user exist, made explicit

MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",    # reads sessionid cookie -> loads session
    "django.contrib.auth.middleware.AuthenticationMiddleware",  # reads session -> sets request.user
]

The auth-and-permissions lesson stated that AuthenticationMiddleware sets request.user by reading the session — this is the literal mechanism: the incoming request's sessionid cookie is looked up, the corresponding session row is loaded (this is what SessionMiddleware does), and AuthenticationMiddleware finds a stored user ID inside that session data, looks up the actual User row, and attaches it as request.user. Every request that "remembers" a logged-in user is doing exactly this lookup chain — cookie, to session, to user — on every single request, not once at login and then somehow remembered afterward.

Why cookies specifically, and not something else

Cookie behavior that makes this work:
- Automatically sent by the browser on every request to the domain — no manual work needed
- Can be marked HttpOnly — inaccessible to JavaScript, reducing XSS risk
- Can be marked Secure — only sent over HTTPS, not plain HTTP

Cookies are the mechanism specifically because browsers handle attaching them automatically — no JavaScript has to manually re-send an identifier on every request. HttpOnly (which Django sets by default on the session cookie) prevents JavaScript from reading the cookie's value at all, which matters directly for the web security lesson's XSS coverage: even if malicious JavaScript gets injected into a page, it can't read and exfiltrate the session cookie if HttpOnly is set. Secure ensures the cookie is never sent over an unencrypted connection, protecting it from being intercepted in transit.

Session expiration: why "still logged in" has a time limit

# settings.py
SESSION_COOKIE_AGE = 1209600   # 2 weeks, in seconds — Django's default
SESSION_EXPIRE_AT_BROWSER_CLOSE = True   # or: expire when the browser closes instead

A session doesn't last forever by default — SESSION_COOKIE_AGE controls how long a session stays valid, and an expired session's cookie is simply treated as if it doesn't exist, sending the request back to being anonymous (AnonymousUser, from the auth lesson) even if the cookie is technically still present. This is a genuine security/convenience trade-off: a longer expiration means users stay logged in longer without re-entering credentials, but also means a stolen or leaked session cookie stays exploitable for longer.

What logging out actually does

from django.contrib.auth import logout
 
def logout_view(request):
    logout(request)   # deletes the session data and clears request.user

logout() doesn't just "forget" the user client-side — it deletes the session data server-side (or at minimum invalidates the reference) and clears request.session, which is exactly why the same sessionid cookie value can't be reused to log back in after a logout; the row it used to point at is gone. This is why session-based logout is more robust than something purely client-side would be — the invalidation happens on the server, which the client's browser has no ability to undo.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Does Django's sessionid cookie actually store the session's data (like cart items or user ID)?

2. What's the actual chain of events that produces request.user on a given request?

3. Why does marking Django's session cookie HttpOnly help against XSS attacks specifically?

4. Why can't a sessionid cookie value be reused to log back in immediately after logout()?