Introduction
Authentication and authorization are essential parts of a production API, but they solve different problems. Authentication identifies the caller; authorization determines what that caller is allowed to do.
Django REST Framework (DRF) provides authentication classes, permission classes, and object-level permission hooks that help developers enforce these rules consistently. The important part is applying them at every relevant boundary, especially when users access resources that belong to other users.
1. Configure authentication explicitly
DRF supports several authentication mechanisms, including session authentication and token-based approaches. The appropriate choice depends on whether the API serves a browser application, mobile clients, third-party integrations, or a combination.
A project can configure default authentication and permission classes in its settings:
REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.TokenAuthentication", ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ],}
This example assumes the corresponding authentication components are installed and configured. Token authentication also requires the appropriate DRF token app and migrations.
A default permission policy of IsAuthenticated is a useful starting point for private APIs. Public endpoints can explicitly opt out when necessary rather than accidentally exposing every endpoint by default.
2. Apply permissions to views
DRF permission classes determine whether a request should be allowed to proceed.
from rest_framework.permissions import IsAuthenticatedfrom rest_framework.viewsets import ModelViewSetclass OrderViewSet(ModelViewSet): serializer_class = OrderSerializer permission_classes = [IsAuthenticated] def get_queryset(self): return Order.objects.filter( customer=self.request.user )
This example assumes the Order model has a customer relationship and that the serializer is defined elsewhere.
The permission class requires authentication, while get_queryset() limits the visible orders to the authenticated user. These are separate controls: requiring login alone does not prevent users from reading other users’ records.
3. Enforce ownership on writes
Filtering the queryset is an important defense, but write operations also need a clear ownership policy. Never trust a request body to decide which user owns a resource.
For a create operation, assign the owner on the server:
from rest_framework.permissions import IsAuthenticatedfrom rest_framework.viewsets import ModelViewSetclass OrderViewSet(ModelViewSet): serializer_class = OrderSerializer permission_classes = [IsAuthenticated] def get_queryset(self): return Order.objects.filter( customer=self.request.user ) def perform_create(self, serializer): serializer.save(customer=self.request.user)
This prevents a client from creating an order on behalf of another customer simply by submitting a different customer ID.
For updates, ensure the object is retrieved through an appropriately restricted queryset or checked with an object-level permission. The correct approach depends on whether the API intentionally allows shared access or administrative access.
4. Use object-level permissions when needed
Some applications need more nuanced rules than simple ownership. A user might edit a document they own, view a document shared with them, and access all documents only if they are an administrator.
A custom permission can express object-level rules:
from rest_framework.permissions import BasePermissionclass IsOwnerOrReadOnly(BasePermission): def has_object_permission( self, request, view, obj ): if request.method in ("GET", "HEAD", "OPTIONS"): return True return obj.owner_id == request.user.id
This example permits read-only access to any object that reaches the permission check, while restricting writes to its owner. It is appropriate only if that read policy matches the application requirements; private resources also need queryset filtering or other checks to prevent unauthorized discovery.
Remember that object-level permission checks are not automatically applied to every item in a list response. Filter list querysets to return only objects the caller is allowed to see.
5. Test the security boundaries
Security tests should verify both allowed and denied behavior. For example:
- An anonymous caller cannot access private orders.
- An authenticated user can access their own order.
- One user cannot retrieve another user’s order by changing the URL ID.
- A user cannot change the owner field through a request body.
- An administrator can perform only the additional actions explicitly granted to administrators.
Test the actual HTTP endpoints, not just the permission class in isolation. A permission rule may be correct but ineffective if a view does not use it or the queryset exposes too much data.
Conclusion
DRF authentication establishes who is making a request, while permissions and queryset restrictions determine what they can access. Apply ownership rules on both reads and writes, use object-level permissions when necessary, and make denied-access scenarios part of the automated test suite.