WRITING / Engineering

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 application programming interface (API) is called 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.

First principles

Hypertext Transfer Protocol (HTTP) is stateless. The server does not remember the previous request, so every request has to carry its own proof of who sent it.

A token is that proof in its simplest form: a long random string the server issued and can look up. Whoever presents it is treated as its owner. The server cannot tell the real client from someone who copied the string.

Everything else in this post follows from that:

  • Issue a token only after a real check of identity, such as a password.
  • Make it possible to take a token away, by deleting it, and to limit how long it works, by expiring it.
  • Never expose it where others can read it: over plain HTTP, in logs, or in Uniform Resource Locators (URLs).

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 sends the token in the Authorization header, prefixed with the word Token. The framework reads it and 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. 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 a certain 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

Expiry changes the login view too. get_or_create would hand back the old, expired token, so login should delete an expired token and issue a fresh one.

Treat tokens like passwords

Serve the API over Hypertext Transfer Protocol Secure (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.

← All writing