anishfyi.com Projects All writings

February 16, 20267 min readDjango, Auth, Security

Token Authentication for a Django API

Setting up token authentication for a Django API, from issuing tokens to rotating and revoking them.

When a Django API is consumed by a single-page app or another service rather than a browser session, token authentication is the straightforward choice. The client holds a token and sends it on every request. Here is how I set it up and, more importantly, how I keep it from becoming a liability.

Token versus session

A session lives in a cookie and a server-side store, which suits a traditional web page. A token is a string the client stores and sends in a header, which suits an API where there is no browser to hold a cookie. Different jobs, so pick by who is calling.

Enable token authentication

INSTALLED_APPS += ["rest_framework.authtoken"]

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

Issue a token on login

from django.contrib.auth import authenticate
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework.views import APIView


class LoginView(APIView):
    authentication_classes = []
    permission_classes = []

    def post(self, request):
        user = authenticate(
            username=request.data.get("username"),
            password=request.data.get("password"),
        )
        if user is None:
            return Response({"detail": "Invalid credentials."}, status=401)
        token, _ = Token.objects.get_or_create(user=user)
        return Response({"token": token.key})

Send it on every request

The client then sends the token in the Authorization header, prefixed with the word Token. The framework reads it, looks up the user, and the rest of the view sees a normal authenticated request.

Rotating and revoking

Logout should delete the token. Rotation is a delete followed by a fresh get_or_create. The point is that a token is a credential, and any credential needs a way to be taken away.

The default token never expires

That is the real weakness. A leaked token works forever. Subclass the authentication class to reject tokens past an age.

from datetime import timedelta
from django.utils import timezone
from rest_framework.authentication import TokenAuthentication
from rest_framework.exceptions import AuthenticationFailed


class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        user, token = super().authenticate_credentials(key)
        if token.created < timezone.now() - timedelta(days=7):
            raise AuthenticationFailed("Token expired.")
        return user, token

Treat tokens like passwords

Serve the API over HTTPS only, so the token is never sent in the clear. Keep tokens out of logs and out of URLs. If you need them to be unreadable at rest, store a hash and compare hashes. A token is a key to the account, and it deserves the same care a password gets.

Working on something in this territory? Book thirty minutes.