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)
SetupMinimal — ships with DRFExtra package, more configuration
ExpirationNone by default — you'd build it yourselfBuilt in — tokens expire and can be refreshed
Token contentsOpaque key, looked up in a database table on every requestSelf-contained — encodes the user info directly, no database lookup needed to verify it
Good fit forSmall APIs, internal tools, quick prototypesPublic-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 classEffect
AllowAnyNo restriction at all — anyone can access, authenticated or not
IsAuthenticatedOnly logged-in users can access, for any method
IsAdminUserOnly users with is_staff=True can access
IsAuthenticatedOrReadOnlyAnyone 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.

Note: All of the authentication and permission code above is correct, real DRF code — but this website's own project doesn't run a live Django server, so none of it can actually be tested from this page.
Next up: With access properly locked down, DRF: Pagination & Filtering covers keeping large result sets manageable and letting clients narrow down what they get back.