DRF: Authentication & Permissions
User Authentication covered logging in with a session and a cookie — that works well for a browser, since the browser stores the session cookie and sends it back automatically on every request. An API client is usually a very different kind of caller: a mobile app, a separate frontend running on its own domain, or an automated script. None of those necessarily keep a browser's cookie jar around, so DRF needs its own way to say "this request is from an authenticated user," independent of Django's session system.
Token Authentication: DRF's Simplest Option
DRF ships a built-in token authentication scheme. Each user gets a single, fixed token string; the client sends that token on every request in an HTTP header, and DRF looks it up to identify the user — no cookie, no session, no login form involved:
# settings.py — enable the app that stores tokens
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}
Generating a token for a user is a one-time step, usually done in the shell or via a dedicated endpoint:
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
user = User.objects.get(username='sonu')
token = Token.objects.create(user=user)
print(token.key) # e.g. 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4
From then on, the client includes that key on every request in an Authorization
header, using the Token scheme:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4
DRF reads that header, looks the key up against the Token table, and attaches the
matching User to request.user — the same attribute session-based views
already rely on, just populated a different way.
A Brief, Honest Mention: JWT
Token authentication above is deliberately simple, and that simplicity is also its main limitation:
a token doesn't expire on its own unless you build that logic yourself, and revoking access means
deleting the row from the database. Many real-world APIs instead reach for
JWT (JSON Web Tokens), typically via the third-party
djangorestframework-simplejwt package.
| Token authentication (built-in) | JWT (via simplejwt) | |
|---|---|---|
| Setup | Minimal — ships with DRF | Extra package, more configuration |
| Expiration | None by default — you'd build it yourself | Built in — tokens expire and can be refreshed |
| Token contents | Opaque key, looked up in a database table on every request | Self-contained — encodes the user info directly, no database lookup needed to verify it |
| Good fit for | Small APIs, internal tools, quick prototypes | Public-facing APIs, mobile apps, anything expecting real production traffic |
Permission Classes: Who Can Access an Endpoint At All
Authentication answers "who is this?" — permissions answer the separate question of "is this person allowed to do this?" DRF ships several ready-made permission classes covering the common cases:
| Permission class | Effect |
|---|---|
AllowAny | No restriction at all — anyone can access, authenticated or not |
IsAuthenticated | Only logged-in users can access, for any method |
IsAdminUser | Only users with is_staff=True can access |
IsAuthenticatedOrReadOnly | Anyone can perform safe, read-only requests (GET); only authenticated users can POST, PUT, PATCH, or DELETE |
IsAuthenticatedOrReadOnly is genuinely one of the most useful defaults for a public API:
it lets anyone browse TaskFlow's tasks, but only a logged-in user can actually create, edit, or
delete one.
Applying Permissions to TaskViewSet
# taskflow/views.py
from rest_framework import viewsets, permissions
from .models import Task
from .serializers import TaskSerializer
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
One extra attribute, and every action on TaskViewSet now respects that rule
automatically — GET /tasks/ stays open to anyone, but a POST without a
valid token gets rejected before it ever reaches the view's own logic.
A Custom Permission: Object-Level Ownership Checks
IsAuthenticatedOrReadOnly only checks "is someone logged in" — it can't express
"only the owner of this task's project can edit it." That kind of per-object rule needs a
custom permission class, implementing has_object_permission(). This is
the same ownership-check idea already covered for regular views in
Authorization & Permissions, just expressed
through DRF's own permission API instead of a plain view check:
# taskflow/permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Safe methods (GET, HEAD, OPTIONS) are always allowed
if request.method in permissions.SAFE_METHODS:
return True
# Otherwise, only the project's owner may edit or delete this task
return obj.project.owner == request.user
DRF calls has_object_permission() once it already has the specific Task
instance in hand — that's the difference from a plain permission check, which only ever sees the
request, not the object it's acting on. Wiring it in is the same pattern as before, just adding a
second class to the list:
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
With both classes applied, reading is open to everyone, writing requires being logged in and passes only if the request also clears the ownership check — anyone trying to edit a task in a project they don't own gets rejected, no matter how valid their token is.