Skip to content

Fix: Django REST Framework 403 Permission Denied

FixDevs ·

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 Authorization header, 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 TokenAuthentication but the client sends a JWT, or expects SessionAuthentication but the client sends a Bearer token.
  • Permission class denies the requestIsAuthenticated rejects unauthenticated users. Custom permission classes may deny based on user role, object ownership, or other conditions.
  • CSRF token missingSessionAuthentication requires a valid CSRF token for non-safe methods (POST, PUT, PATCH, DELETE). Browser-based clients must include the X-CSRFToken header.
  • Global default permissions too restrictiveDEFAULT_PERMISSION_CLASSES in settings.py applies to all views. If set to IsAuthenticated, every endpoint requires login by default.
  • Object-level permission deniedget_object() calls check_object_permissions(). A permission class that implements has_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:

ClassBehavior
AllowAnyNo restrictions — any request is allowed
IsAuthenticatedRequest must be authenticated
IsAdminUserUser must have is_staff = True
IsAuthenticatedOrReadOnlyRead (GET, HEAD, OPTIONS) is open; write requires auth
DjangoModelPermissionsRequires Django model-level permissions (add_, change_, delete_, view_)
DjangoObjectPermissionsPer-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 migrate

Create 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.user

Fix 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 response

Test 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/False

Check 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 created

Check 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles