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 theAuthorizationheader. Right for mobile apps and scripts — clients with no browser session. Enable it by adding"rest_framework.authtoken"toINSTALLED_APPSand 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 needspatients.add_patientto 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_REDIRECTmatters 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
Authorizationheader, never the query string. - Be able to revoke — deleting a user's
Tokenrow 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
- Enable
TokenAuthentication(addauthtoken, migrate), create a token for a user, and make an authenticated request withAuthorization: Token …; confirm 200. Make one without the header and confirm 401/403. - Set
IsAuthenticatedas the default permission and confirm anonymous requests are rejected. - Switch a read endpoint to
IsAuthenticatedOrReadOnly; confirm anonymous GET works but anonymous POST is rejected. - Scope a ViewSet's
get_querysettorequest.userso a caller sees only their own records; verify one user cannot fetch another's. - Try
DjangoModelPermissionsand confirm a user withoutadd_patientcannot POST. - Reason about which scheme Nidaan's patient mobile app should use versus its same-site staff dashboard, and why.
Official documentation
- DRF — Authentication — Session, Token, and custom schemes.
- DRF — Permissions — The permission classes and object-level permissions.
- djangorestframework-simplejwt — JWT, when you need it.
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