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.