API Authentication, CORS, and Frontend Integration
Introduction
Single-page apps (Vue, React) on localhost:5173 calling Django on localhost:8000 hit CORS browser rules and need token-based auth instead of cookie-only session forms. This chapter configures django-cors-headers, compares Session vs Token vs JWT authentication for DRF, protects write endpoints, and outlines Nginx same-origin deployment for production.
Prerequisites
- Django REST Framework Basics —
PostViewSetat/api/v1/ - User Authentication — Django
Usermodel - Forms and CSRF — CSRF vs API tokens
CORS Problem
Browsers block JavaScript from reading responses when:
- Frontend origin:
http://localhost:5173 - API origin:
http://127.0.0.1:8000
Different host or port = cross-origin. Server must send Access-Control-Allow-Origin.
Install django-cors-headers
pip install django-cors-headerssettings/base.py:
Development:
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173",
"http://127.0.0.1:5173",
]Or permissive dev only:
# development.py only — never in production
CORS_ALLOW_ALL_ORIGINS = TrueProduction: explicit origins or same-domain Nginx proxy (no CORS needed).
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
]
CORS_ALLOW_CREDENTIALS = True # if using session cookies cross-subdomainAuthentication Options
| Method | Best for | Header / cookie |
|---|---|---|
| Session | Same-site browser app | Session cookie + CSRF |
| Token | Mobile, simple APIs | Authorization: Token <key> |
| JWT | SPAs, microservices | Authorization: Bearer <jwt> |
Session authentication (DRF built-in)
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
],
}SPA must send CSRF token with POST:
const csrftoken = document.cookie.match(/csrftoken=([^;]+)/)?.[1];
fetch("http://127.0.0.1:8000/api/v1/posts/", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrftoken,
},
body: JSON.stringify({ title: "Hi", slug: "hi", body: "x", published: true }),
});Same-origin deploy avoids some CORS pain—see Nginx below.
Token Authentication
Enable authtoken app:
INSTALLED_APPS = [
...
"rest_framework.authtoken",
]python manage.py migrateREST_FRAMEWORK:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
}Create token for user (shell or view):
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=user)
print(token.key)Obtain via API endpoint — add blog/urls_api.py:
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
path("auth/token/", obtain_auth_token),
path("", include(router.urls)),
]curl -X POST http://127.0.0.1:8000/api/v1/auth/token/ \
-d "username=alice&password=pass12345"Response: {"token":"abc123..."}
Authenticated request:
curl http://127.0.0.1:8000/api/v1/posts/ \
-H "Authorization: Token abc123..."JWT with simplejwt (Concept)
For stateless SPAs:
pip install djangorestframework-simplejwtcurl -X POST http://127.0.0.1:8000/api/v1/auth/jwt/ \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"pass12345"}'Use Authorization: Bearer <access>; refresh with refresh token before expiry.
Compare: Spring Security JWT, Flask JWT.
Permission Classes
blog/viewsets.py:
Import BasePermission from rest_framework.permissions.
Only authors edit/delete their posts; anyone reads published list (filter in get_queryset).
Frontend Fetch Example (Token)
const API = "http://127.0.0.1:8000/api/v1";
const token = localStorage.getItem("token");
async function loadPosts() {
const res = await fetch(`${API}/posts/`, {
headers: { Authorization: `Token ${token}` },
});
return res.json();
}Vue/React axios interceptors attach the same header globally. Hello Code Vue track covers fetch, Vite proxy, and admin auth project with JWT guards.
Production: Same Origin via Nginx
Serve SPA and API under one domain—browser sees same origin, CORS optional.
Django URLs stay /api/v1/ — align Nginx location /api/ with your prefix.
See Nginx reverse proxy.
CSRF Exempt for Token APIs?
Token/JWT auth typically does not use CSRF—DRF Token auth bypasses CSRF when not using session. Do not @csrf_exempt session-based views carelessly.
Security Notes
| Topic | Practice |
|---|---|
CORS_ALLOW_ALL_ORIGINS | Dev only |
| Token storage | httpOnly cookies vs localStorage XSS tradeoff |
| JWT expiry | Short access token + refresh rotation |
| HTTPS | Required in production — Let's Encrypt |
Test Authenticated API
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| CORS middleware too late | Browser CORS error | CorsMiddleware high in list |
Missing Authorization header | 401 | Token/JWT on every write |
| Session API without CSRF | 403 | CSRF header or switch to Token |
AllowAny on delete | Open API | IsAuthenticated + object perms |
| Wildcard CORS + credentials | Browser rejects | Explicit origins |
Post-Chapter Checklist
-
django-cors-headersconfigured for dev frontend origin - Token or JWT auth protects POST/PUT/DELETE
-
IsAuthorOrReadOnly(or similar) onPostViewSet - curl/Postman can obtain token and create post
- You understand same-origin Nginx vs CORS
FAQ## FAQ
Cookie-based SPA auth?
Possible with SessionAuthentication + CSRF + credentials: "include"—same-site cookies preferred.
Revoke tokens?
Delete Token row or use JWT blocklist (simplejwt BLACKLIST app).
OAuth / social login?
django-allauth or python-social-auth—beyond this track.
Preflight OPTIONS?
corsheaders handles OPTIONS automatically.
GraphQL?
graphene-django—separate stack; REST track uses DRF.