~/TechPurAI
~/tutorials/django-rest-framework/token-authentication
intermediate·part 15 of 22·2 min read

Authentication: token-based API auth

Updated Aug 16, 2026Python · Django

Every request so far has worked with no login at all — request.user in perform_create() has quietly been AnonymousUser this entire series unless tested from a browser with an active Django admin session. This part fixes that properly, with authentication built for an API client rather than a browser.

Why session auth (the Django default) doesn't fit an API

Django's default authentication relies on a session cookie, set when logging in through a browser-rendered form. A mobile app, a script, or a separate frontend calling this API has no browser session to carry — API authentication instead expects the client to send proof of identity with every single request, typically as a header, which is exactly what token authentication provides.

Setting up TokenAuthentication

python
# taskapi/settings.py
INSTALLED_APPS = [
    # ...
    "rest_framework.authtoken",
]

REST_FRAMEWORK = {
    # ... existing settings ...
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
}
bash
python manage.py migrate

rest_framework.authtoken is its own app with its own model (Token, one per user) and needs a migration the same as any other. Keeping SessionAuthentication alongside TokenAuthentication is deliberate — it's what lets the browsable API (part 18) keep working with your existing Django admin login, while TokenAuthentication handles everything else.

Issuing a token

python
# tasks/urls.py
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = router.urls + [
    path("token/", obtain_auth_token, name="api-token"),
]
bash
curl -X POST http://127.0.0.1:8000/api/token/ -d "username=alice&password=testpass123"
json
{"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4"}

obtain_auth_token is a built-in DRF view — it validates a username/password and returns the associated Token in one call, no view code of your own needed. A real client calls this once, stores the token, and sends it on every subsequent request from then on.

Sending it on every request

bash
curl http://127.0.0.1:8000/api/tasks/ -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4"

The Authorization: Token <value> header format — Token, not Bearer — is specific to DRF's TokenAuthentication; other schemes (JWT among them) use Bearer instead, worth knowing since copying the wrong prefix from a different tutorial is a common source of a mysterious 401.

Authentication is not permissions

python
request.user               # the User if a valid token was sent, AnonymousUser otherwise
request.user.is_authenticated

TokenAuthentication only identifies who is making the request — it doesn't, by itself, block anyone. Right now, an anonymous request to TaskViewSet still succeeds; the crucial distinction is that authentication answers "who is this?" while permissions (next part) answer "what are they allowed to do?" — two separate, composable layers.

Common mistake

Sending the token over plain HTTP instead of HTTPS. A token is a long-lived credential — unlike a short-lived session cookie's typical lifecycle, a leaked API token often keeps working until manually revoked. Never send one anywhere except over HTTPS in production.

Next: actually restricting what an authenticated (or anonymous) user is allowed to do, with DRF's permission classes.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous14. Filtering, searching, and ordering resultsnext →16. Permissions: built-in classes and writing a custom one