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.