Fix: Django REST Framework 403 Permission Denied
Quick Answer
How to fix Django REST Framework 403 Forbidden and permission denied errors — authentication classes, permission classes, IsAuthenticated vs AllowAny, object-level permissions, and CSRF issues.
The Error
A Django REST Framework API endpoint returns a 403 Forbidden response:
{
"detail": "Authentication credentials were not provided."
}Or:
{
"detail": "You do not have permission to perform this action."
}Or a CSRF-related 403:
403 Forbidden
CSRF Failed: CSRF token missing or incorrect.Or a token authentication error:
{
"detail": "Invalid token."
}Why This Happens
DRF uses a two-step security model: authentication (who are you?) followed by authorization (what are you allowed to do?). A 403 can come from either step failing:
- Missing or invalid authentication credentials — the request doesn’t include an
Authorizationheader, or the token is expired/invalid. DRF returns"Authentication credentials were not provided."even though the status code is 403 (or sometimes 401 depending on the authentication class). - Wrong authentication class — the view expects
TokenAuthenticationbut the client sends a JWT, or expectsSessionAuthenticationbut the client sends a Bearer token. - Permission class denies the request —
IsAuthenticatedrejects unauthenticated users. Custom permission classes may deny based on user role, object ownership, or other conditions. - CSRF token missing —
SessionAuthenticationrequires a valid CSRF token for non-safe methods (POST, PUT, PATCH, DELETE). Browser-based clients must include theX-CSRFTokenheader. - Global default permissions too restrictive —
DEFAULT_PERMISSION_CLASSESinsettings.pyapplies to all views. If set toIsAuthenticated, every endpoint requires login by default. - Object-level permission denied —
get_object()callscheck_object_permissions(). A permission class that implementshas_object_permission()may reject access to a specific object even when the list-level permission passes.
Fix 1: Set the Correct Permission Class on the View
Override the permission classes on specific views to control who can access them:
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser
from rest_framework.views import APIView
from rest_framework.response import Response
# Require authentication for this specific view
class PrivateView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
return Response({'data': 'sensitive'})
# Allow any user (no authentication required)
class PublicView(APIView):
permission_classes = [AllowAny]
def get(self, request):
return Response({'data': 'public'})
# Admin only
class AdminOnlyView(APIView):
permission_classes = [IsAdminUser]
def get(self, request):
return Response({'data': 'admin only'})With ViewSets:
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.decorators import action
class PostViewSet(ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
def get_permissions(self):
"""Different permissions for different actions."""
if self.action in ('list', 'retrieve'):
# Public read access
permission_classes = [AllowAny]
else:
# Authenticated write access
permission_classes = [IsAuthenticated]
return [permission() for permission in permission_classes]Built-in permission classes:
| Class | Behavior |
|---|---|
AllowAny | No restrictions — any request is allowed |
IsAuthenticated | Request must be authenticated |
IsAdminUser | User must have is_staff = True |
IsAuthenticatedOrReadOnly | Read (GET, HEAD, OPTIONS) is open; write requires auth |
DjangoModelPermissions | Requires Django model-level permissions (add_, change_, delete_, view_) |
DjangoObjectPermissions | Per-object permissions using Django’s object permission framework |
Fix 2: Fix the Global Default Permission and Authentication Settings
Check settings.py to understand what applies globally to all views:
# settings.py
REST_FRAMEWORK = {
# Global default — applied to all views without explicit permission_classes
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', # ← Requires auth everywhere
# 'rest_framework.permissions.AllowAny', # ← Open (insecure for APIs)
],
# Authentication methods to try, in order
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
# 'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}To open specific views while keeping global auth:
# Override per-view — don't change the global default
class RegisterView(APIView):
permission_classes = [AllowAny] # Override the global IsAuthenticated
authentication_classes = [] # No authentication needed
def post(self, request):
# Handle registration
...Fix 3: Fix TokenAuthentication — Include the Token Correctly
DRF’s TokenAuthentication requires the token in the Authorization header with the format Token <token-value>:
# WRONG — missing "Token" prefix
curl -H "Authorization: abc123" http://localhost:8000/api/data/
# WRONG — using "Bearer" prefix (that's JWT, not DRF token auth)
curl -H "Authorization: Bearer abc123" http://localhost:8000/api/data/
# CORRECT — DRF TokenAuthentication format
curl -H "Authorization: Token abc123" http://localhost:8000/api/data/Set up token authentication properly:
# settings.py
INSTALLED_APPS = [
...
'rest_framework.authtoken', # ← Must be in INSTALLED_APPS
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}# Run migrations to create the token table
python manage.py migrateCreate a login endpoint that returns a token:
# views.py
from rest_framework.authtoken.views import obtain_auth_token
# urls.py
from django.urls import path
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
path('api/token/', obtain_auth_token), # POST with username/password → returns token
]# Get a token
curl -X POST http://localhost:8000/api/token/ \
-d '{"username": "alice", "password": "secret"}' \
-H "Content-Type: application/json"
# {"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"}
# Use the token
curl -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" \
http://localhost:8000/api/data/Create tokens for existing users:
# Django shell
python manage.py shell
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
user = User.objects.get(username='alice')
token, created = Token.objects.get_or_create(user=user)
print(token.key)Fix 4: Fix SessionAuthentication CSRF Errors
SessionAuthentication requires a valid CSRF token for write operations. This is by design — it protects against cross-site request forgery:
# The 403 with CSRF message:
# "CSRF Failed: CSRF token missing or incorrect."For browser-based JavaScript clients:
// Get CSRF token from the cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
const csrfToken = getCookie('csrftoken');
// Include in all non-safe requests
fetch('/api/posts/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken, // ← Required for POST/PUT/PATCH/DELETE
},
body: JSON.stringify({ title: 'New Post' }),
});For API clients (mobile apps, third-party services) that use token or JWT authentication — use TokenAuthentication or JWTAuthentication instead. These don’t require CSRF:
# API clients (non-browser) — use token auth, which doesn't need CSRF
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication', # No CSRF required
'rest_framework.authentication.SessionAuthentication', # Browser clients
],
}Exempt specific views from CSRF (use with caution):
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view
# Only do this if you have another form of authentication (token/JWT)
@csrf_exempt
@api_view(['POST'])
def webhook_endpoint(request):
# Webhook from external service — no CSRF token available
...Warning: Never exempt CSRF from endpoints that use session authentication without another security mechanism. CSRF exemption on session-based endpoints opens your API to cross-site request forgery attacks.
Fix 5: Write Custom Permission Classes
For ownership-based or role-based access control, write a custom permission:
# permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsOwnerOrReadOnly(BasePermission):
"""
Object-level permission: allow read for anyone,
write only if the user owns the object.
"""
def has_permission(self, request, view):
# Allow read (GET, HEAD, OPTIONS) for all
if request.method in SAFE_METHODS:
return True
# Write requires authentication
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# Read is allowed for everyone
if request.method in SAFE_METHODS:
return True
# Write only if user owns the object
return obj.owner == request.user
class IsVerifiedUser(BasePermission):
"""Only allow verified (email-confirmed) users."""
message = 'Please verify your email address before accessing this resource.'
def has_permission(self, request, view):
return (
request.user and
request.user.is_authenticated and
request.user.profile.email_verified # Custom user profile field
)# views.py
class PostViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
serializer_class = PostSerializer
def get_queryset(self):
return Post.objects.filter(published=True)
def perform_create(self, serializer):
# Automatically set owner to the current user
serializer.save(owner=self.request.user)Combine multiple permission classes (AND logic):
# All permission classes must return True
permission_classes = [IsAuthenticated, IsVerifiedUser, IsOwnerOrReadOnly]OR logic with custom operator:
from rest_framework.permissions import BasePermission
class IsAdminOrOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.is_staff or obj.owner == request.userFix 6: Debug Permission Issues
Enable DRF exception detail in development:
# settings.py (development only)
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myapp.exceptions.custom_exception_handler',
}# exceptions.py
from rest_framework.views import exception_handler
import logging
logger = logging.getLogger(__name__)
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None and response.status_code in (401, 403):
# Log which permission class denied the request
request = context['request']
view = context['view']
logger.warning(
f"Permission denied: {exc} | "
f"User: {request.user} | "
f"View: {view.__class__.__name__} | "
f"Method: {request.method}"
)
return responseTest permission logic directly in Django shell:
python manage.py shell
from django.test import RequestFactory
from django.contrib.auth.models import User
from rest_framework.test import APIRequestFactory
from myapp.permissions import IsOwnerOrReadOnly
from myapp.models import Post
factory = APIRequestFactory()
# Simulate a request from a specific user
user = User.objects.get(username='alice')
post = Post.objects.first()
request = factory.get('/')
request.user = user
permission = IsOwnerOrReadOnly()
print(permission.has_permission(request, None)) # True/False
print(permission.has_object_permission(request, None, post)) # True/FalseCheck which authentication and permission classes are active on a view:
python manage.py shell
from myapp.views import PostViewSet
view = PostViewSet()
print(view.get_authenticators())
print(view.get_permissions())Still Not Working?
Check for missing perform_authentication call. Some custom middleware or view logic may need to explicitly authenticate:
class MyView(APIView):
def get(self, request):
# Force authentication resolution
request.user # Accessing this triggers authentication
if request.user.is_authenticated:
return Response({'data': 'ok'})
return Response({'detail': 'Not authenticated'}, status=401)Verify the token exists in the database. Tokens can be deleted or expired:
python manage.py shell
from rest_framework.authtoken.models import Token
# Check if a token exists
Token.objects.filter(key='9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b').exists()
# → False means the token was deleted or never createdCheck Django REST Framework’s browsable API — visit the endpoint in a browser while logged into Django admin. If it works there but not via the API client, the issue is in how the client sends authentication credentials.
For JWT authentication (djangorestframework-simplejwt), the token prefix is Bearer, not Token:
# SimpleJWT uses Bearer prefix
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
http://localhost:8000/api/data/# settings.py with SimpleJWT
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}For related Django issues, see Fix: Django Migration Error and Fix: Spring Security 403 Forbidden.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.
Fix: OpenAI API Not Working — RateLimitError, 401, 429, and Connection Issues
How to fix OpenAI API errors — RateLimitError (429), AuthenticationError (401), APIConnectionError, context length exceeded, model not found, and SDK v0-to-v1 migration mistakes.
Fix: Python Packaging Not Working — Build Fails, Package Not Found After Install, or PyPI Upload Errors
How to fix Python packaging issues — pyproject.toml setup, build backends (setuptools/hatchling/flit), wheel vs sdist, editable installs, package discovery, and twine upload to PyPI.
Fix: Celery Beat Not Working — Scheduled Tasks Not Running or Beat Not Starting
How to fix Celery Beat issues — beat scheduler not starting, tasks not executing on schedule, timezone configuration, database scheduler, and running beat with workers.