RizTech Academy logo
RizTech Academy
Building an API with DRFLesson 4 of 530 min

Token and session authentication for APIs

An API needs to know who is calling and what they may do — the same authentication and permissions as your HTML pages, but arranged for clients that are not browsers with sessions. A mobile app, a script, another server: each needs a way to prove its identity on every request. DRF separates this into authentication (who are you?) and permissions (may you?), and gives you several schemes. This lesson covers the common ones and, importantly, when each is appropriate.

Authentication versus permissions, again

The distinction from the auth module carries straight over, and DRF makes it explicit as two separate settings:

  • Authentication identifies the caller — it sets request.user. DRF tries each configured authentication scheme until one identifies the request (or leaves it anonymous).
  • Permissions decide whether that identified caller may perform the action — checked after authentication.

You configure defaults globally and override per view:

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}

The authentication schemes, and when to use each

DRF ships several; the ones you will actually choose between:

  • SessionAuthentication — uses Django's normal session cookie. Right when your API is consumed by a frontend on the same site (a JavaScript app served from the same Django project) — the user is already logged in via the session, and the API rides on it. It relies on cookies, so it also needs CSRF protection for unsafe methods.
  • TokenAuthentication — each user has a token; the client sends it in the Authorization header. Right for mobile apps and scripts — clients with no browser session. Enable it by adding "rest_framework.authtoken" to INSTALLED_APPS and migrating; then issue a token per user:
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=user)

The client sends it on every request:

GET /api/patients/
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

Verified: a request carrying a valid Authorization: Token … header authenticates and the endpoint returns 200. TokenAuthentication is the simplest scheme for non-browser clients — one long-lived token per user, sent in a header.

  • JWT (via djangorestframework-simplejwt) — short-lived, self-contained tokens with refresh, common in larger or multi-service setups. More moving parts (access/refresh tokens, expiry); reach for it when you need stateless, expiring tokens or interoperate with other services, not by default.

The honest guidance: SessionAuthentication for a same-site frontend, TokenAuthentication for mobile apps and scripts, JWT when you specifically need expiring stateless tokens. Do not reach for JWT reflexively — it is more complex than many projects need.

Permissions on the API

DRF permission classes gate access, set globally or per view:

  • IsAuthenticated — must be logged in (the common default).
  • AllowAny — open, for genuinely public endpoints.
  • IsAuthenticatedOrReadOnly — anyone may read, only authenticated users may write.
  • DjangoModelPermissions — ties the API to the same model permissions (add/change/delete) from the auth module, so a user needs patients.add_patient to POST.
from rest_framework.permissions import IsAuthenticated

class PatientViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated]
    # ...

For object-level rules — a patient seeing only their own records — DRF has has_object_permission on a custom permission class, but the queryset-scoping technique from the object-level-permissions lesson applies just as well here: override get_queryset to return only what the caller may see. The three-layer model (authenticated → model permission → object scope) is identical on the API; DRF just gives you the hooks.

Tokens are credentials — treat them accordingly

A token is a password-equivalent: whoever has it can act as that user. So the same care applies:

  • Only ever send tokens over HTTPS — a token sent over plain HTTP can be sniffed and reused. (Production forces HTTPS; the security-defaults lesson's SECURE_SSL_REDIRECT matters here too.)
  • Store tokens securely on the client — a mobile app's secure storage, not a logfile or a URL.
  • Never put a token in a URL — URLs are logged, cached and shared; credentials belong in the Authorization header, never the query string.
  • Be able to revoke — deleting a user's Token row immediately invalidates it (an advantage of simple tokens over stateless JWTs, which remain valid until they expire unless you build a revocation list).

These are the same instincts as SECRET_KEY and passwords: a token is secret, so it travels only over encrypted channels, is stored safely, and can be revoked. An API's security is only as good as how its clients handle their tokens.

Check your work

Authentication versus permissions in DRF. Authentication sets request.user (who); permissions decide if they may act (may you) — configured as DEFAULT_AUTHENTICATION_CLASSES and DEFAULT_PERMISSION_CLASSES, overridable per view.

The schemes and when. SessionAuthentication for a same-site frontend (rides the session cookie, needs CSRF); TokenAuthentication for mobile/scripts (a per-user token in the Authorization header — verified 200 with a valid token); JWT only when you need expiring stateless tokens.

Permission classes. IsAuthenticated, AllowAny, IsAuthenticatedOrReadOnly, DjangoModelPermissions (ties to model permissions); object-level via get_queryset scoping or has_object_permission.

Tokens are credentials. HTTPS only, stored securely on the client, never in a URL, and revocable by deleting the token row.

Practice

  1. Enable TokenAuthentication (add authtoken, migrate), create a token for a user, and make an authenticated request with Authorization: Token …; confirm 200. Make one without the header and confirm 401/403.
  2. Set IsAuthenticated as the default permission and confirm anonymous requests are rejected.
  3. Switch a read endpoint to IsAuthenticatedOrReadOnly; confirm anonymous GET works but anonymous POST is rejected.
  4. Scope a ViewSet's get_queryset to request.user so a caller sees only their own records; verify one user cannot fetch another's.
  5. Try DjangoModelPermissions and confirm a user without add_patient cannot POST.
  6. Reason about which scheme Nidaan's patient mobile app should use versus its same-site staff dashboard, and why.

Official documentation

Next: testing your API.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship