Django: implement HTTP bearer authentication

Authentication is key.

HTTP has a general authentication framework that defines a pattern into which various authentication schemes can fit. Clients may provide an authorization request header that contains a credential. If authorization is missing or invalid, the server may respond with a 401 (Unauthorized) status code, including a www-authenticate header advertising what authentication schemes are supported. Otherwise, the server can respond with the authenticated resource.

The simplest authentication scheme in this framework is called Bearer, where the client needs to provide a valid token (typically a randomly generated string) in authorization. Bearer authentication is commonly used in APIs, where it’s convenient to provide a single token rather than a username and password.

Django-based API frameworks provide built-in support for Bearer authentication, such as Django REST Framework’s TokenAuthentication or django-ninja’s authentication layer. But for simple cases, such frameworks can be a bit heavyweight, and you might want to implement Bearer authentication yourself, which only takes a few lines of code.

Here’s an example implementing single-token Bearer authentication for a Django view returning JSON data:

import os
import secrets
from http import HTTPStatus

from django.http import JsonResponse

SECRET_STUFF_TOKEN = os.environ.get("SECRET_STUFF_TOKEN", "")


def secret_stuff(request):
    authorization = request.headers.get("authorization", "")
    if not SECRET_STUFF_TOKEN or not secrets.compare_digest(
        f"Bearer {SECRET_STUFF_TOKEN}", authorization
    ):
        return JsonResponse(
            {"detail": "Unauthorized"},
            status=HTTPStatus.UNAUTHORIZED,
            headers={
                "www-authenticate": 'Bearer realm="Secret area!"',
            },
        )

    return JsonResponse({"answer": 42})

In this example, there’s a single valid token, stored in the SECRET_STUFF_TOKEN environment variable. The view code checks if the request’s authorization header matches the expected value, using secrets.compare_digest() to avoid timing attacks.

If the header is invalid, the view responds with an appropriate 401 Unauthorized response, including a www-authenticate header advertising the Bearer scheme. Otherwise, the token is assumed valid and the view returns the secret data (the answer is 42!).

We can test this view with some unit tests using Django’s test client:

from http import HTTPStatus
from unittest import mock

from django.test import SimpleTestCase

from example import views


class SecretStuffTests(SimpleTestCase):
    def test_unauthorized_no_header(self):
        response = self.client.get("/secret-stuff/")

        assert response.status_code == HTTPStatus.UNAUTHORIZED
        assert response.json() == {"detail": "Unauthorized"}
        assert "www-authenticate" in response.headers
        assert response.headers["www-authenticate"] == 'Bearer realm="Secret area!"'

    def test_unauthorized_invalid_token(self):
        response = self.client.get(
            "/secret-stuff/",
            headers={"authorization": "Bearer hunter1"},
        )

        assert response.status_code == HTTPStatus.UNAUTHORIZED
        assert response.json() == {"detail": "Unauthorized"}
        assert "www-authenticate" in response.headers
        assert response.headers["www-authenticate"] == 'Bearer realm="Secret area!"'

    def test_authorized(self):
        secret_token = "hunter2"
        with mock.patch.object(views, "SECRET_STUFF_TOKEN", secret_token):
            response = self.client.get(
                "/secret-stuff/",
                headers={"authorization": f"Bearer {secret_token}"},
            )

            assert response.status_code == HTTPStatus.OK
            assert response.json() == {"answer": 42}

These tests cover the three cases: no authorization header, an incorrect token, and the correct token respectively.

Make a decorator to reduce repetitive repetition

For authenticating multiple views, it’s convenient to extract the authentication logic into a view decorator, like:

import functools
import os
import secrets
from http import HTTPStatus

from django.http import JsonResponse

SECRET_STUFF_TOKEN = os.environ.get("SECRET_STUFF_TOKEN", "")


def bearer_auth(view_func):
    @functools.wraps(view_func)
    def wrapper(request, *args, **kwargs):
        authorization = request.headers.get("authorization", "")
        if not SECRET_STUFF_TOKEN or not secrets.compare_digest(
            f"Bearer {SECRET_STUFF_TOKEN}", authorization
        ):
            return JsonResponse(
                {"detail": "Unauthorized"},
                status=HTTPStatus.UNAUTHORIZED,
                headers={
                    "www-authenticate": 'Bearer realm="Secret area!"',
                },
            )
        return view_func(request, *args, **kwargs)

    return wrapper


@bearer_auth
def secret_answer(request):
    return JsonResponse({"answer": 42})


@bearer_auth
def secret_tip(request):
    return JsonResponse({"tip": "The cake is a lie."})

Extensions

Having a single hardcoded token may be all you need, for example for an internal API that will only be accessed by a single trusted client. But in more complex scenarios, you may want to support multiple tokens, token revocation, token expiration, or scopes/permissions. In these cases, you might want to extend the above code to use tokens stored in a database model. And if you need a lot of features, it may be time to consider a framework like Django REST Framework or django-ninja.

Fin

May the bearer of this blog post implement authentication exceedingly successfully,

—Adam


😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: