Introduction
Token authentication is the standard for API security. Instead of sending credentials with every request, clients receive a token after login and include it in subsequent requests. This approach is stateless, scalable, and works across different domains and platforms.
Token authentication is the standard for API security. Instead of sending credentials with every request, clients receive a token after login.
Key Concepts
API Token: A unique, random string that identifies a user. Stored in the database and sent via the Authorization header.
Bearer Authentication: The standard format for sending tokens: Authorization: Bearer <token>.
Token Lifecycle: Tokens are created on login, sent with every request, and can expire or be revoked.
Middleware: A Django component that intercepts requests to extract and validate tokens before they reach your views.
Real World Context
Token authentication is used in:
- Mobile applications: Tokens stored securely on device
- Third-party API access: Developers get API keys for their applications
- Microservices: Services authenticate with each other using tokens
- CLI tools: Command-line tools that interact with APIs
How Token Auth Works
1. Client sends username/password to login endpoint
2. Server validates credentials and returns a token
3. Client stores the token
4. Client sends token in Authorization header with every request
5. Server validates token and identifies the user
Creating a Token Model
The token model stores a cryptographically random key linked to a user, with optional expiration and usage tracking:
python# models.py import secrets from django.db import models from django.conf import settings class APIToken(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='api_tokens' ) key = models.CharField(max_length=64, unique=True, db_index=True) name = models.CharField(max_length=100, blank=True) # e.g., "Mobile App" created_at = models.DateTimeField(auto_now_add=True) last_used_at = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) def save(self, *args, **kwargs): if not self.key: self.key = secrets.token_hex(32) # 64 character hex string super().save(*args, **kwargs) def __str__(self): return f"{self.user.username} - {self.name or 'API Token'}" @property def is_expired(self): if self.expires_at is None: return False from django.utils import timezone return timezone.now() > self.expires_at
The save() override auto-generates a 64-character hex token using secrets.token_hex(32) when no key is set. The is_expired property checks against the current time.
Login Endpoint
This view accepts credentials via POST, validates them with Django's authenticate(), and returns a token the client stores for future requests:
python# views.py import json from django.http import JsonResponse from django.contrib.auth import authenticate from django.views.decorators.csrf import csrf_exempt from .models import APIToken @csrf_exempt def api_login(request): if request.method != 'POST': return JsonResponse({'error': 'Method not allowed'}, status=405) try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) username = data.get('username') password = data.get('password') if not username or not password: return JsonResponse({'error': 'Username and password required'}, status=400) user = authenticate(username=username, password=password) if user is None: return JsonResponse({'error': 'Invalid credentials'}, status=401) if not user.is_active: return JsonResponse({'error': 'Account disabled'}, status=401) # Create or get token token, created = APIToken.objects.get_or_create( user=user, name='default' ) return JsonResponse({ 'token': token.key, 'user': { 'id': user.id, 'username': user.username, 'email': user.email } })
The get_or_create call ensures each user gets one default token. For multiple tokens per user (mobile, desktop), pass a unique name for each.
Token Authentication Middleware
This middleware intercepts every request, extracts the Bearer token from the Authorization header, and attaches the corresponding user to request.user:
python# middleware.py from django.utils import timezone from .models import APIToken class TokenAuthMiddleware: """Authenticate requests using Bearer tokens.""" def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # Get Authorization header auth_header = request.headers.get('Authorization', '') if auth_header.startswith('Bearer '): token_key = auth_header[7:] # Remove 'Bearer ' prefix try: token = APIToken.objects.select_related('user').get(key=token_key) if not token.is_expired: request.user = token.user request.auth_token = token # Update last used token.last_used_at = timezone.now() token.save(update_fields=['last_used_at']) except APIToken.DoesNotExist: pass # Invalid token, user remains anonymous return self.get_response(request)
Add the middleware to your Django settings so it runs on every request:
python# settings.py MIDDLEWARE = [ # ... other middleware 'myapp.middleware.TokenAuthMiddleware', ]
The middleware uses select_related('user') to fetch the user in the same query as the token lookup, and tracks last_used_at for security auditing.
Authentication Decorator
With the middleware setting request.user, you can protect individual views with a simple decorator that checks authentication:
python# decorators.py from functools import wraps from django.http import JsonResponse def api_login_required(view_func): """Require authentication for API views.""" @wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse( {'error': 'Authentication required'}, status=401 ) return view_func(request, *args, **kwargs) return wrapper # Usage @api_login_required def api_profile(request): return JsonResponse({ 'id': request.user.id, 'username': request.user.username, 'email': request.user.email })
Views decorated with @api_login_required return 401 for unauthenticated requests, letting the middleware handle token validation separately from view logic.
Client Usage
The most secure approach for web apps is to have the server set tokens as httpOnly cookies:
python# Server-side: return token via httpOnly cookie @csrf_exempt def api_login(request): # ... authenticate user ... token, created = APIToken.objects.get_or_create(user=user, name='default') response = JsonResponse({ 'user': {'id': user.id, 'username': user.username} }) response.set_cookie( 'api_token', token.key, httponly=True, # Cannot be read by JavaScript (XSS-safe) secure=True, # HTTPS only in production samesite='Lax', # CSRF protection max_age=86400, # 24 hours ) return response
javascript// Client-side: cookies are sent automatically const response = await fetch('/api/login/', { method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin', // Include cookies body: JSON.stringify({username: 'john', password: 'secret'}) }); // WARNING: Do NOT use localStorage for tokens! // localStorage is vulnerable to XSS attacks. // The httpOnly cookie above is automatically included in requests. // Subsequent requests include the cookie automatically const articles = await fetch('/api/articles/', { credentials: 'same-origin' });
The httponly=True flag is critical: it prevents JavaScript from reading the cookie, making it immune to XSS attacks. The credentials: 'same-origin' option tells fetch to include cookies automatically.
Common Pitfalls
-
Storing tokens insecurely: Never store tokens in localStorage for web apps. Use httpOnly cookies to prevent XSS attacks.
-
No token expiration: Tokens that never expire are a security risk. Always set an expiration date.
-
Not using HTTPS: Tokens sent over HTTP can be intercepted. Always use HTTPS in production.
Best Practices
-
Use cryptographically secure tokens: Use
secrets.token_hex()orsecrets.token_urlsafe()for token generation. -
Set token expiration: Tokens should have a reasonable lifetime (hours to days, not forever).
-
Track token usage: Record
last_used_atfor security auditing and inactive token cleanup. -
Support multiple tokens per user: Allow users to have separate tokens for different devices or applications.
Summary
Token authentication provides stateless API security by issuing unique tokens on login. Implement tokens with a database model, validate them via middleware, and protect views with authentication decorators. Always use secure storage (httpOnly cookies for web), set expiration dates, and use HTTPS in production.
Code Examples
import secrets
from django.db import models
from django.conf import settings
class APIToken(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
key = models.CharField(max_length=64, unique=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if not self.key:
self.key = secrets.token_hex(32)
super().save(*args, **kwargs)
@property
def is_expired(self):
if self.expires_at is None:
return False
from django.utils import timezone
return timezone.now() > self.expires_at