Auth in Django + React: Build the Intuition
Stop copy-pasting auth code you don't understand — learn the mental models behind sessions, tokens, JWTs, and OAuth
Technology: Django REST Framework, React, JWT, OAuth 2.0
Skill: Authentication, Backend Engineering, Security
The one question auth always answers
HTTP is stateless. The server remembers nothing between requests. So after you log in, how does the server know it's still you on request #2?
The answer is always the same shape:
- You prove who you are (login)
- The server hands you a credential of some kind
- You send that credential back on every future request
Every auth strategy is just a different answer to: what is that credential, and how does the server verify it?
The core tradeoff: stateful vs stateless
This is the most important axis to understand.
- Stateful auth — the server stores a record (a session row, a token row) and looks it up on every request. The credential is just a key into that record. Instant revocation: delete the row and the credential is dead.
- Stateless auth — the credential is the proof. The server verifies it cryptographically without touching the database. Faster at scale, but you can't revoke a credential until it expires.
A second axis: cookies vs headers.
- Cookies attach to requests automatically — the browser handles it. Great for server-rendered apps, awkward cross-origin (requires CORS + CSRF configuration).
Authorizationheaders must be set manually by your JavaScript. Natural for React SPAs, no CSRF problem.
Strategy 1: Session auth
What it is: Django's built-in mechanism. The server writes a session record to the database and gives the browser a sessionid cookie.
The flow:
# Django creates a session row and sets the cookie
from django.contrib.auth import authenticate, login
user = authenticate(username=..., password=...)
login(request, user) # sets sessionid cookie in the response
On every subsequent request, the browser sends the cookie automatically. Django looks up the session ID, finds the user.
When to use it: Same-origin setups only — Django serving your React build from the same domain. The moment your React dev server (localhost:3000) talks to Django (localhost:8000), you're cross-origin and the cookie dance becomes painful.
Key properties:
- Stateful — every session is a database row
- Instant revocation
- Requires CSRF tokens on every mutating request (because cookies are sent automatically, CSRF attacks are possible)
- Doesn't scale without a shared session store like Redis
Strategy 2: DRF Token auth
What it is: DRF's built-in token system. A Token row lives in the database, one per user. The client stores it and sends it as a header.
# settings.py
INSTALLED_APPS = ['rest_framework.authtoken']
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
]
}
// React — after login, attach the token to every request
fetch('/api/data/', {
headers: { 'Authorization': `Token ${token}` } // note: "Token", not "Bearer"
});
When to use it: Simple APIs, internal tools, early-stage projects. It's the easiest thing that works cross-origin.
Key properties:
- Stateful — every request hits the
authtoken_tokentable - No CSRF needed (header-based)
- Tokens never expire by default — you have to build that yourself
- One token per user — no per-device granularity without customization
Strategy 3: JWT
What it is: A JSON Web Token is a self-contained credential. Instead of looking up a database record, the server verifies a cryptographic signature.
The structure: header.payload.signature — three base64-encoded segments.
// Payload (readable by anyone — never put secrets here)
{
"user_id": 42,
"email": "user@example.com",
"exp": 1716239022
}
The signature is computed with your SECRET_KEY. Tamper with the payload → signature mismatch → request rejected. No database needed.
The access + refresh pattern is the key insight:
- Access token — short-lived (15 minutes). Sent on every request. If stolen, it expires fast.
- Refresh token — long-lived (7 days). Only used to get a new access token. Store it more carefully.
// Obtain a token pair on login
const { access, refresh } = await fetch('/api/token/', {
method: 'POST',
body: JSON.stringify({ username, password })
}).then(r => r.json());
// Attach the access token to every API call
fetch('/api/projects/', {
headers: { 'Authorization': `Bearer ${access}` }
});
// When the access token expires, silently refresh it
const { access: newAccess } = await fetch('/api/token/refresh/', {
method: 'POST',
body: JSON.stringify({ refresh })
}).then(r => r.json());
When to use it: This is the right default for a standalone React + DRF project. Stateless verification means any backend instance can handle any request — horizontal scaling is free.
Key properties:
- Stateless — no DB lookup to verify
- No instant revocation for access tokens (use short lifetimes as the mitigation)
- Payload is base64-encoded, not encrypted — anyone can read it
- Use
djangorestframework-simplejwt
Strategy 4: Google OAuth + OpenID Connect
What it is: You outsource credential storage to Google. Your app never sees a password.
The key artifact is Google's id_token — a JWT signed by Google's private key. Your backend verifies it against Google's public keys, extracts the user identity, then issues your own JWT. After that, Google is out of the picture.
The flow:
- React redirects the user to Google's auth page
- Google redirects back with an authorization
code - Your Django backend exchanges the
codefor anid_token(server-to-server) - Backend verifies the
id_token, finds or creates the user - Backend issues its own SimpleJWT access + refresh pair
- From here on, auth is identical to pure JWT
One critical detail: Use the sub claim (Google's internal user ID) as your foreign key to Google — not the email address. Emails can change. sub never does.
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests
claims = id_token.verify_oauth2_token(
raw_id_token,
google_requests.Request(),
settings.GOOGLE_CLIENT_ID
)
google_id = claims['sub'] # stable forever
email = claims['email'] # could change
When to use it: Consumer apps where users already have Google accounts. You get MFA, breach detection, and password storage for free.
Key properties:
- Your app stores no passwords (set
user.set_unusable_password()) - Provider dependency — if Google's auth is down, your login is down
- More moving parts, but the result integrates cleanly with JWT
Choosing a strategy
- Session auth — only if same-origin. Avoid for cross-origin React + Django setups.
- DRF Token — simplest option. Fine for low traffic. Watch out for the missing expiry.
- JWT (SimpleJWT) — the right default for a decoupled React SPA. Short access tokens, refresh token rotation, blacklist for revocation.
- Google OAuth — add when you want passwordless login. Terminate the flow with a SimpleJWT pair and the frontend never knows the difference.
These strategies compose. A production app might use JWT as the single verification mechanism, but issue that JWT through multiple entry points — username/password at /api/token/ and Google OAuth at /api/auth/google/callback/. The React frontend doesn't care which path the user took. It just holds a Bearer token and moves on.