Authentication vs Authorization
These terms are often confused. In HTTP, the distinction is reflected in status codes:
- Authentication — proving who you are. Failure returns 401 Unauthorized (which should really be called "unauthenticated"). The response includes a
WWW-Authenticateheader that tells the client how to authenticate. - Authorization — proving you're allowed to do something. Failure returns 403 Forbidden. You've proven who you are, but you lack permission.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.example.com"
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"error": "insufficient_scope", "required_scope": "admin"}
Pattern 1: Bearer Tokens
Bearer tokens are credentials that grant access to whoever holds them. The holder ("bearer") doesn't need to prove they generated the token — possession is sufficient. JWTs are the most common bearer token format.
How It Works
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyJ9...
The client includes the token in every request. The server validates the token (usually by verifying a signature or looking it up in a database) and identifies the user.
Token Validation
Two approaches:
- Self-contained (JWT): The server verifies the token's signature using a public key. No database lookup needed. Fast, but tokens cannot be revoked until expiry.
- Reference token (opaque): The server looks up the token in a database or cache. Slower (one DB hit per request), but tokens can be revoked instantly.
# Django: bearer token authentication
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt
class JWTAuthentication(BaseAuthentication):
def authenticate(self, request):
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return None # No credentials — let other authenticators try
token = auth_header[7:]
try:
payload = jwt.decode(
token,
settings.JWT_PUBLIC_KEY,
algorithms=['RS256'],
)
user = User.objects.get(id=payload['sub'])
return (user, token)
except (jwt.InvalidTokenError, User.DoesNotExist):
raise AuthenticationFailed('Invalid token')
Token Refresh Flow
Access tokens should be short-lived (5-60 minutes). A separate long-lived refresh token is used to obtain new access tokens without re-authenticating:
POST /auth/token/refresh
Content-Type: application/json
{"refresh_token": "long-lived-opaque-token"}
HTTP/1.1 200 OK
{"access_token": "new-short-lived-jwt", "expires_in": 3600}
Pattern 2: API Keys
API keys are long random strings issued to developers for programmatic access. They're simpler than OAuth but less granular.
Header vs Query Parameter
# Preferred: Authorization header (not logged by default)
curl -H 'X-API-Key: sk_live_abc123...' https://api.example.com/data
# Alternative: Bearer format
curl -H 'Authorization: ApiKey sk_live_abc123...' https://api.example.com/data
# Avoid: Query parameter (logged in access logs, server logs, referrer headers)
curl 'https://api.example.com/data?api_key=sk_live_abc123...'
Never put API keys in URLs. URLs are logged everywhere — access logs, browser history, CDN logs, referrer headers. A key in a URL is a leaked key.
Key Design Best Practices
import secrets
import hashlib
def create_api_key(user):
# Generate key with a prefix for easy identification
key = 'sk_live_' + secrets.token_urlsafe(32)
# Store only the hash — never the plaintext key
key_hash = hashlib.sha256(key.encode()).hexdigest()
ApiKey.objects.create(
user=user,
key_hash=key_hash,
prefix=key[:12], # For identification without full exposure
)
# Return the plaintext key to the user once — never again
return key
Show the full key only once at creation. Display only the prefix thereafter (e.g., sk_live_abc1...). This means if a key is compromised, you can identify which key it was without the database being a risk.
Key Rotation
Support multiple active keys per user to enable zero-downtime rotation:
- User creates a new key
- User updates their application to use the new key
- User deletes the old key
Send email alerts when a key hasn't been used for 90 days (may be forgotten somewhere risky) and when a key is first used from a new IP range.
Pattern 3: Session Cookies
Session cookies are the traditional web authentication mechanism. A session ID stored in a cookie is looked up in a server-side session store on each request.
The Setup
POST /auth/login
Content-Type: application/json
{"username": "alice", "password": "..."}
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123xyz; HttpOnly; Secure; SameSite=Lax; Path=/
The browser automatically sends the cookie with every subsequent request to the same origin. The server looks up abc123xyz in Redis or the database to find the associated user.
CSRF Protection
Because cookies are sent automatically with cross-origin requests (for some SameSite configurations), session cookies are vulnerable to Cross-Site Request Forgery (CSRF). A malicious page can trigger state-changing requests to your API if the user is logged in.
Defenses:
- SameSite=Strict — cookie not sent on any cross-origin request (breaks OAuth callbacks)
- SameSite=Lax (default) — cookie sent on top-level navigations, not on sub-resource loads
- CSRF tokens — include a secret token in forms, verify it server-side
# Django: CSRF is enabled by default for session-authenticated views
# For API endpoints using session auth, use Django REST Framework's session auth:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication', # Enforces CSRF
]
}
Choosing the Right Pattern
| Client Type | Recommended Pattern | Why |
|---|---|---|
| Server-to-server (microservices) | Bearer token (JWT or opaque) | No user context needed; token can carry service identity |
| Web SPA | Bearer token (short-lived JWT) + refresh token in HttpOnly cookie | Protects against XSS; refresh token survives page reloads |
| Mobile app | Bearer token via OAuth + PKCE | System browser protects credentials; OS keychain stores tokens |
| Third-party developer | API key | Simple to issue, rotate, and revoke; scoped permissions |
| Traditional web app | Session cookie | Browser handles automatically; server-side revocation is instant |
| IoT / embedded device | API key | Cannot run OAuth flows; key stored in secure storage |
For mixed clients (a product used by both browsers and third-party integrations), support both patterns: session cookies for browser users, API keys or OAuth for developers. Keep the authentication layer pluggable so you can add methods without restructuring your permission logic.