Stop hand-rolling token auth. Production-grade JWT for Django REST Framework: access/refresh tokens with rotation, blacklist-on-logout, secure storage (httpOnly cookies vs localStorage), and OAuth2 social auth with allauth.
Token authentication is where most Django REST APIs ship a foot-gun. The default tutorials show you a 10-line setup that's wrong for production: tokens that never expire, no refresh rotation, no logout, and storage choices that hand attackers your sessions. This guide is the version you actually deploy.
Django sessions are excellent. They're stateful (server can revoke instantly), CSRF-protected, and battle-tested. If your client is a browser on the same origin as your API, just use sessions.
JWT shines when:
JWT hurts when: you trust localStorage, you skip rotation, or you can't revoke. Don't use it just because it's trendy.
pip install djangorestframework-simplejwt
# settings.py
from datetime import timedelta
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True, # NEW refresh on every refresh
"BLACKLIST_AFTER_ROTATION": True, # old refresh token unusable
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"AUTH_HEADER_TYPES": ("Bearer",),
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
}
INSTALLED_APPS += [
"rest_framework_simplejwt.token_blacklist",
]
Then run python manage.py migrate to create the blacklist tables.
from rest_framework_simplejwt.views import (
TokenObtainPairView, TokenRefreshView, TokenBlacklistView,
)
urlpatterns += [
path("api/auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("api/auth/logout/", TokenBlacklistView.as_view(), name="token_blacklist"),
]
With ROTATE_REFRESH_TOKENS=True + BLACKLIST_AFTER_ROTATION=True:
/api/auth/token/refresh/ with refresh token R1.R2.R1 is added to the blacklist.If R1 is ever used again (say, an attacker stole it), the request fails. That's how you detect token theft — log the failure as a security event and force re-login.
Two choices, and they have very different threat models:
| Option | Stops | Doesn't stop |
|---|---|---|
| localStorage | CSRF | XSS — any script on your domain reads the token |
| httpOnly cookie | XSS theft | CSRF (mitigate with SameSite=Lax/Strict + CSRF token) |
For browser apps: httpOnly + Secure + SameSite=Lax cookies. Set them server-side after login:
response.set_cookie(
"access_token", str(access),
httponly=True, secure=True, samesite="Lax",
max_age=15*60, path="/api/",
)
response.set_cookie(
"refresh_token", str(refresh),
httponly=True, secure=True, samesite="Strict",
max_age=7*24*3600, path="/api/auth/token/refresh/",
)
Subclass JWTAuthentication to read from cookies instead of Authorization. The path scoping on the refresh cookie limits its blast radius.
pip install django-allauth dj-rest-auth
For Google sign-in: register an OAuth2 client in Google Cloud Console, store the client_id/secret in .env, and let allauth handle the auth code + PKCE flow. dj-rest-auth wraps allauth in REST endpoints that return SimpleJWT tokens — you get OAuth2 inbound + JWT outbound with ~30 lines of config and zero hand-rolled crypto.
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token["org_id"] = user.org_id
token["role"] = user.role
return token
Never put PII or secrets in claims. JWT payloads are base64 — not encrypted. Anyone with the token can read them. Put only what's safe to be public-knowable.
SESSION_COOKIE_SECURE=True + SECURE_SSL_REDIRECT=True.RS256 with a key pair if multiple services verify tokens — they only need the public key.kid verifying for one TTL./api/auth/token/ aggressively (5/min/IP). Brute-force attempts will find you.token_blacklist hit. If a known-blacklisted token is presented, you've witnessed an attack.Use SimpleJWT, turn on rotation + blacklist, store tokens in httpOnly cookies for browsers, and let allauth handle social OAuth2. The point of all of this isn't "JWT good" — it's that you have a story for theft, logout, and key rotation. Without those, JWT is just a worse session.