Django Authentication Deep Dive: JWT, Sessions, and Custom Backends
Go beyond Django's built-in auth — learn JWT, custom email login, role-based permissions, and brute-force protection. Intermediate | Read Time: 12 min | Author: [SRI BALU]
Introduction
Authentication is the backbone of almost every web application. Django ships with a solid built-in auth system — but in real-world projects, you'll quickly outgrow it. Whether you're building a REST API, a multi-tenant SaaS, or a social login platform, understanding Django's authentication internals gives you the power to customize it exactly how you need.
In this deep-dive, we'll cover:
- How Django's authentication system works under the hood
- Session-based vs JWT-based authentication
- Implementing JWT authentication with
djangorestframework-simplejwt - Writing a custom authentication backend (e.g., login with email instead of username)
- Securing your endpoints with permissions
1. How Django Authentication Works Under the Hood
Before writing any code, it helps to understand what happens when a user logs in.
Django's auth system revolves around three core components:
| Component | Role |
|---|---|
User model |
Stores credentials (username, password hash) |
AuthenticationBackend |
Validates credentials |
Session / Token
|
Maintains state after login |
When you call authenticate(request, username=..., password=...), Django loops through all backends listed in AUTHENTICATION_BACKENDS and returns the first User object that matches — or None.
# django/contrib/auth/__init__.py (simplified)
def authenticate(request=None, **credentials):
for backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
try:
user = backend.authenticate(request, **credentials)
except PermissionDenied:
return None
if user is None:
continue
user.backend = backend_path
return user
return None
The default backend is django.contrib.auth.backends.ModelBackend, which checks the database for a matching username + password hash.
2. Session-Based Authentication (The Django Default)
Django's default auth uses server-side sessions. Here's the flow:
User submits login form
→ Django validates credentials
→ Creates a session in the DB (django_session table)
→ Sends a session cookie (sessionid) to the browser
→ On every request, browser sends the cookie
→ Django looks up the session → finds the user
Setting it up (already built-in):
# settings.py
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.sessions',
...
]
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
...
]
Login/Logout views:
# views.py
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import redirect, render
def login_view(request):
if request.method == 'POST':
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
return redirect('dashboard')
else:
return render(request, 'login.html', {'error': 'Invalid credentials'})
return render(request, 'login.html')
def logout_view(request):
logout(request)
return redirect('login')
When to use sessions:
- Traditional Django web apps (not APIs)
- Server-rendered templates
- When you control both frontend and backend
3. JWT Authentication for REST APIs
For Django REST Framework (DRF) APIs — especially those consumed by React, Flutter, or mobile apps — JWT (JSON Web Tokens) is the preferred approach.
Why JWT over sessions for APIs?
| Sessions | JWT | |
|---|---|---|
| State | Server-side | Stateless |
| Scalability | Requires shared DB | Works across servers |
| Mobile support | Cookie issues | Easy via headers |
| Logout | Simple | Requires token blacklist |
Installation:
pip install djangorestframework djangorestframework-simplejwt
# settings.py
INSTALLED_APPS = [
...
'rest_framework',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'AUTH_HEADER_TYPES': ('Bearer',),
}
URL configuration:
# urls.py
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenBlacklistView,
)
urlpatterns = [
path('api/auth/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/auth/logout/', TokenBlacklistView.as_view(), name='token_blacklist'),
]
Protecting API views:
# views.py
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profile_view(request):
user = request.user
return Response({
'id': user.id,
'username': user.username,
'email': user.email,
})
How to call the API:
# Step 1: Get tokens
curl -X POST http://localhost:8000/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{"username": "john", "password": "secret123"}'
# Response:
# {"access": "eyJ0eXAiOiJKV...", "refresh": "eyJ0eXAiOiJKV..."}
# Step 2: Call protected endpoint
curl -X GET http://localhost:8000/api/profile/ \
-H "Authorization: Bearer eyJ0eXAiOiJKV..."
4. Custom Authentication Backend: Login with Email
By default, Django uses username to authenticate. In modern apps, users expect to log in with their email address. Here's how to write a custom backend:
# backends.py
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
User = get_user_model()
class EmailBackend(ModelBackend):
"""
Authenticate using email instead of username.
Falls back to username if email not found.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
try:
# Try to find user by email
user = User.objects.get(email=username)
except User.DoesNotExist:
# Fall back to username lookup
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return None
if user.check_password(password) and self.user_can_authenticate(user):
return user
return None
Register the backend:
# settings.py
AUTHENTICATION_BACKENDS = [
'myapp.backends.EmailBackend',
'django.contrib.auth.backends.ModelBackend', # keep as fallback
]
Now authenticate(request, username="john@example.com", password="secret") will work automatically — no other code changes needed!
5. Custom JWT Payload (Add Extra Claims)
By default, JWT tokens contain only user_id. You can add extra data like role, email, or is_staff:
# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add custom claims
token['email'] = user.email
token['username'] = user.username
token['is_staff'] = user.is_staff
token['role'] = getattr(user, 'role', 'user') # if you have a role field
return token
# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
# urls.py
path('api/auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
6. Role-Based Permissions
Once authentication is set up, you'll want authorization — controlling what users can do.
Built-in permission classes:
from rest_framework.permissions import (
IsAuthenticated, # Must be logged in
IsAdminUser, # Must be staff/superuser
IsAuthenticatedOrReadOnly, # Read for all, write for authenticated
)
Custom permission class:
# permissions.py
from rest_framework.permissions import BasePermission
class IsOwnerOrAdmin(BasePermission):
"""
Allow access only to object owners or admin users.
"""
def has_object_permission(self, request, view, obj):
if request.user.is_staff:
return True
return obj.owner == request.user
# views.py
from rest_framework import generics
from .permissions import IsOwnerOrAdmin
from .models import Post
from .serializers import PostSerializer
class PostDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [IsAuthenticated, IsOwnerOrAdmin]
7. Security Best Practices
✅ Always do this:
# settings.py
# Use HTTPS in production
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Prevent XSS from stealing session cookies
SESSION_COOKIE_HTTPONLY = True
# Limit session age
SESSION_COOKIE_AGE = 1209600 # 2 weeks in seconds
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 8}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
✅ Rate-limit login attempts:
pip install django-axes
# settings.py
INSTALLED_APPS = ['axes', ...]
AUTHENTICATION_BACKENDS = [
'axes.backends.AxesStandaloneBackend',
'myapp.backends.EmailBackend',
]
AXES_FAILURE_LIMIT = 5 # Lock after 5 failed attempts
AXES_COOLOFF_TIME = 1 # Unlock after 1 hour
Summary
Here's what we covered:
| Topic | Use Case |
|---|---|
| Session auth | Traditional Django web apps |
| JWT with simplejwt | REST APIs, mobile apps |
| Custom EmailBackend | Login with email |
| Custom JWT claims | Pass role/email in token |
| Custom permissions | Role-based access control |
| django-axes | Brute-force protection |
Django's auth system is designed to be extended — not replaced. By understanding how backends, sessions, and tokens work together, you can build secure, scalable authentication for any use case.
What's Next?
-
Part 2: OAuth2 & Social Login with
django-allauth(Google, GitHub) - Part 3: Two-Factor Authentication (2FA) with TOTP
- Part 4: Multi-tenant authentication with row-level security
Found this helpful? Share it on Twitter and tag me! Have questions? Drop them in the comments below.
Tags: django python authentication jwt django-rest-framework web-development backend
Top comments (0)