Django REST API authentication: best practices explained
Authentication is often where backend teams make their first critical architectural choice: pick the wrong one for your client type, and you'll spend months retrofitting. Django REST API authentication: best practices explained means matching your auth mechanism to whether you're serving web browsers, mobile apps, or other services. We'll walk through the three main patterns, show you how to layer permissions and throttling on top, and highlight the mistakes we've seen slow teams down in production.
Matching auth mechanisms to your client types
The biggest mistake is treating all clients the same. A same-domain web app, a mobile app, and a server calling your API should use different authentication strategies. Let's break down the three primary options:
Session authentication for same-domain web applications
If your frontend and backend live on the same domain (or you control both), session-based authentication remains the simplest choice. The client sends credentials once, the server creates a session, and subsequent requests include a session cookie automatically handled by the browser.
# settings.py
INSTALLED_APPS = [
'rest_framework',
'rest_framework.authtoken',
# ...
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
class AccountView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
return Response({'username': request.user.username})
Sessions work well because browsers handle cookies automatically, CSRF protection is built in (via Django's middleware), and users stay logged in across page refreshes. However, session servers don't scale horizontally without sticky sessions, and they're unsuitable for mobile clients that need stateless auth.
JWT (JSON Web Tokens) for mobile and single-page applications
Mobile apps and modern SPAs can't rely on browser cookies. JWT provides a stateless token the client stores and sends with each request. The server verifies the token's signature without querying a session store.
# settings.py
INSTALLED_APPS = [
'rest_framework_simplejwt',
# ...
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
# views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def protected_endpoint(request):
return Response({'user_id': request.user.id})
A client posts credentials to /api/token/, receives an access token (short-lived, minutes to hours) and a refresh token (longer-lived, days to weeks), then includes the access token in the Authorization: Bearer <token> header. When the access token expires, the client uses the refresh token to get a new one without re-entering credentials. This pattern scales horizontally because the server doesn't store state.
API keys and OAuth client credentials for server-to-server
When one service needs to call another, neither session cookies nor user JWTs fit. API keys work for simple cases: a client includes a key in a header, the server looks it up and associates it with a specific service account.
# models.py
from rest_framework.authtoken.models import Token
# Token is created automatically when a User is created if you use signals,
# or manually: Token.objects.create(user=service_account_user)
# views.py
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
class WebhookView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
# request.user is the service account
return Response({'received': True})
For more complex scenarios—scoped access, revocation without re-issuing, delegated permissions—OAuth 2.0 client credentials flow is standard. Libraries like django-oauth-toolkit handle the flow; the server grants the calling service a bearer token valid for specific scopes.
Layering permissions on top of authentication
Authentication answers "who are you?" Permissions answer "are you allowed to do this?" Django REST Framework separates the two: once a request is authenticated, permission classes decide if the user can proceed.
Built-in permission classes
Start with the framework's built-in classes:
- IsAuthenticated — any logged-in user can access.
- IsAdminUser — only staff=True users.
- IsAuthenticatedOrReadOnly — anyone can read, only authenticated users can write.
- AllowAny — no restrictions (useful for signup, password reset).
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.views import APIView
from rest_framework.response import Response
class ArticleView(APIView):
permission_classes = [IsAuthenticatedOrReadOnly]
def get(self, request, pk):
# Anyone can read
article = Article.objects.get(pk=pk)
return Response(article.data)
def put(self, request, pk):
# Only authenticated users can update
article = Article.objects.get(pk=pk)
article.update(request.data)
return Response(article.data)
Custom permission classes for business logic
Often you need to encode your domain rules. Custom permission classes inherit from BasePermission and implement has_permission for view-level checks.
from rest_framework.permissions import BasePermission
class IsSubscribed(BasePermission):
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
return request.user.subscription_status == 'active'
class ListSubscribersView(APIView):
permission_classes = [IsSubscribed]
def get(self, request):
subscribers = Subscriber.objects.filter(user=request.user)
return Response(SubscriberSerializer(subscribers, many=True).data)
Object-level permissions for ownership checks
View-level permissions are too coarse when you need to check if a user owns the resource they're accessing. The has_object_permission method runs after has_permission and receives the target object.
from rest_framework.permissions import BasePermission
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
# Write permissions are only allowed to the owner
return obj.owner == request.user
class PostDetailView(APIView):
permission_classes = [IsOwnerOrReadOnly]
def get(self, request, pk):
post = Post.objects.get(pk=pk)
self.check_object_permissions(request, post)
return Response(PostSerializer(post).data)
def put(self, request, pk):
post = Post.objects.get(pk=pk)
self.check_object_permissions(request, post)
serializer = PostSerializer(post, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)
When using generic views, the framework calls check_object_permissions automatically. Make sure to query the object and call the method explicitly if you're using plain APIView.
Rate limiting with throttle classes
Authentication opens the door; throttling prevents abuse. Throttle classes rate-limit requests per user, per IP, or per client identifier. This protects your backend from being hammered and ensures fair resource allocation.
Built-in throttle classes
- AnonRateThrottle — limits anonymous (unauthenticated) users by IP.
- UserRateThrottle — limits authenticated users by username.
- ScopedRateThrottle — uses different limits for different views based on a scope attribute.
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '1000/hour',
},
}
# views.py
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView
class ContactUsView(APIView):
throttle_classes = [UserRateThrottle] # Override default
def post(self, request):
# Even authenticated users can only POST once per minute in this view
# (if you set the scope separately)
return Response({'sent': True})
Custom throttle classes for fine-grained control
For payment or high-value operations, you might throttle more aggressively. Inherit from BaseThrottle and implement allow_request:
from rest_framework.throttling import BaseThrottle
from django.core.cache import cache
import time
class PaymentThrottle(BaseThrottle):
def allow_request(self, request, view):
if not request.user.is_authenticated:
return True # Anonymous users aren't making payments
cache_key = f'payment_{request.user.id}'
last_payment = cache.get(cache_key)
if last_payment is None:
cache.set(cache_key, time.time(), 3600) # 1 hour expiry
return True
time_since_last = time.time() - last_payment
if time_since_last < 60: # Min 60 seconds between payments
return False
cache.set(cache_key, time.time(), 3600)
return True
def throttle_success(self):
return True
class CheckoutView(APIView):
throttle_classes = [PaymentThrottle]
def post(self, request):
# Process payment
return Response({'transaction_id': '...'})
Common mistakes
Forgetting to call check_object_permissions in custom views: If you write your own APIView subclass, manually call self.check_object_permissions(request, obj) before returning data. Generic views do this for you, but plain views don't.
Using session auth for mobile clients: Mobile apps can't reliably store and send cookies; you'll hit CORS and state-management issues. Use JWT instead.
Setting overly generous token expiry times: An access token with a 30-day lifetime is a security liability. Keep them short (minutes to a few hours) and use refresh tokens for longer sessions.
Applying throttles globally when only some endpoints need them: Throttling a login endpoint the same as a list endpoint wastes rate limits. Use scoped throttles or override at the view level.
Storing sensitive data in JWT claims: JWTs are signed, not encrypted. Anyone with the token can read the payload. Don't include passwords, secrets, or PII.
Key takeaways
- Choose session auth for same-domain web apps, JWT for mobile and SPAs, and API keys or OAuth for server-to-server calls.
- Always pair authentication with permission classes;
IsAuthenticatedis a baseline, but custom classes encode your business rules. - Use
has_object_permissionfor ownership checks on specific resources; don't assume all authenticated users have equal access. - Throttle by user or IP to prevent abuse; custom throttle classes let you enforce domain-specific rate limits (payment operations, signup, etc.).
- In production, keep JWT access tokens short-lived (15 min to 1 hour), store refresh tokens securely, and monitor authentication failures for suspicious patterns.






Comments
Loading comments…