2025-12-08
Previously, we covered bearer authentication within HTTP’s general authentication framework. In this post, we’ll implement basic authentication, where the client provides a username and password.
To recap, HTTP’s general authentication framework defines a general scheme for authentication:
- Clients may provide an authorization request header that contains a credential.
- Servers validate the header and respond with either the requested resource o…
2025-12-08
Previously, we covered bearer authentication within HTTP’s general authentication framework. In this post, we’ll implement basic authentication, where the client provides a username and password.
To recap, HTTP’s general authentication framework defines a general scheme for authentication:
- Clients may provide an authorization request header that contains a credential.
- Servers validate the header and respond with either the requested resource or a 401 (Unauthorized) status code that includes a www-authenticate response header advertising what authentication schemes are supported.
Basic authentication is an authentication scheme within the framework that browsers natively support. When accessing a page protected with basic authentication, browsers show a login prompt to the user, like this one in Firefox:

After the user enters their credentials, the browser sends a new request with the authorization header set to the string Basic <credentials>, where <credentials> is a base64-encoded string of the form <username>:<password>. The server can then validate the credentials and respond accordingly.
Basic authentication is not the best user experience, as the browser login dialog cannot be styled and password managers cannot autofill it. It also requires some security considerations, as the credentials are sent with every request to the protected resource, increasing the risk of exposure, but if you use HTTPS, as basically required for the modern web, this risk is somewhat mitigated. Despite these downsides, basic authentication is convenient for simple use cases, like temporarily password-protecting a work-in-progress page, as it’s fast to implement without any HTML, CSS, or JavaScript. And sometimes it’s just plain required to integrate with a legacy system.
Django doesn’t provide built-in basic authentication, but it takes minimal code to implement it yourself. Django REST Framework does provide BasicAuthentication, but it’s not a particularly convenient authentication method for APIs.
Here is a complete example of how to implement Basic authentication for a Django view:
import base64
import os
import secrets
from http import HTTPStatus
from django.shortcuts import render
USERNAME = os.environ.get("SECRET_STUFF_USERNAME", "")
PASSWORD = os.environ.get("SECRET_STUFF_PASSWORD", "")
def secret_stuff(request):
authorization = request.headers.get("authorization", "")
if not USERNAME or not PASSWORD:
# Unconfigured, deny all access
return _unauthorized(request)
if not authorization.startswith("Basic "):
return _unauthorized(request)
authorization = authorization.removeprefix("Basic ")
try:
credentials = base64.b64decode(authorization).decode("utf-8")
username, password = credentials.split(":", 1)
except (ValueError, UnicodeDecodeError):
return _unauthorized(request)
username_matches = secrets.compare_digest(username, SECRET_STUFF_USERNAME)
password_matches = secrets.compare_digest(password, SECRET_STUFF_PASSWORD)
if not username_matches or not password_matches:
return _unauthorized(request)
return render("secret_stuff.html", request)
def _unauthorized(request):
response = render(
request,
"unauthorized.html",
status=HTTPStatus.UNAUTHORIZED,
)
response.headers["www-authenticate"] = 'Basic realm="Secret area!"'
return response
For this example, there’s a single valid username-password pair, provided through environment variables. If those environment variables are not set, the view denies all access, otherwise it checks that the authorization header is present and correctly formatted. Basic authentication credentials are base64-encoded strings of the form username:password, so the view decodes and splits the credentials accordingly, then compares them with the expected values using secrets.compare_digest() to avoid timing attacks.
If any step of the validation fails, the view responds with a 401 Unauthorized response, with the www-authenticate header advertising the Basic scheme. This header prompts browsers to show the login dialog. If the credentials are valid, the view renders the protected content.
To test this view, we can use Django’s test client:
from base64 import b64encode
from http import HTTPStatus
from unittest import mock
from django.test import SimpleTestCase
from example import views
@mock.patch.multiple(
views,
SECRET_STUFF_USERNAME="admin",
SECRET_STUFF_PASSWORD="hunter2",
)
class SecretStuffTests(SimpleTestCase):
def test_unauthorized_no_header(self):
response = self.client.get("/secret-stuff/")
assert response.status_code == HTTPStatus.UNAUTHORIZED
assert "<h1>Unauthorized</h1>" in response.text
assert response.headers["www-authenticate"] == 'Basic realm="Secret area!"'
def test_unauthorized_unconfigured(self):
with mock.patch.multiple(
views, SECRET_STUFF_USERNAME="", SECRET_STUFF_PASSWORD=""
):
credentials = b64encode(b"admin:hunter2").decode()
response = self.client.get(
"/secret-stuff/",
headers={"authorization": f"Basic {credentials}"},
)
assert response.status_code == HTTPStatus.UNAUTHORIZED
assert "<h1>Unauthorized</h1>" in response.text
assert response.headers["www-authenticate"] == 'Basic realm="Secret area!"'
def test_unauthorized_wrong_authorization_type(self):
response = self.client.get(
"/secret-stuff/",
headers={"authorization": "Bearer sometoken"},
)
assert response.status_code == HTTPStatus.UNAUTHORIZED
assert "<h1>Unauthorized</h1>" in response.text
assert response.headers["www-authenticate"] == 'Basic realm="Secret area!"'
def test_unauthorized_malformed_credentials(self):
malformed_credentials = b64encode(b"malformedcredentials").decode()
response = self.client.get(
"/secret-stuff/",
headers={"authorization": f"Basic {malformed_credentials}"},
)
assert response.status_code == HTTPStatus.UNAUTHORIZED
assert "<h1>Unauthorized</h1>" in response.text
def test_unauthorized_wrong_username(self):
wrong_credentials = b64encode(b"wrong:hunter2").decode()
response = self.client.get(
"/secret-stuff/",
headers={"authorization": f"Basic {wrong_credentials}"},
)
assert response.status_code == HTTPStatus.UNAUTHORIZED
assert "<h1>Unauthorized</h1>" in response.text
assert response.headers["www-authenticate"] == 'Basic realm="Secret area!"'
def test_unauthorized_wrong_password(self):
wrong_credentials = b64encode(b"admin:wrongpassword").decode()
response = self.client.get(
"/secret-stuff/",
headers={"authorization": f"Basic {wrong_credentials}"},
)
assert response.status_code == HTTPStatus.UNAUTHORIZED
assert "<h1>Unauthorized</h1>" in response.text
def test_authorized_access(self):
credentials = b64encode(b"admin:hunter2").decode()
response = self.client.get(
"/secret-stuff/",
headers={"authorization": f"Basic {credentials}"},
)
assert response.status_code == HTTPStatus.OK
assert "<h1>🤫 Secret stuff</h1>" in response.text
These tests cover all paths through the code, giving 100% coverage.
mock.patch.multiple() is particularly neat for setting the expected username and password during the tests, overriding any environment variable configuration.
Extensions
It may be sufficient to support just a single hardcoded username and password pair, as above, for example for a demo site that only a few trusted users will access. But in more complex scenarios, you may want to support multiple users and passwords, Django’s user model integration, rate limiting, and so on. You can certainly extend the above code to support those features, though at some point it will probably make sense to switch to a classic login form instead, typically through Django’s authentication framework.
Fin
May you find basic authentication a basic topic,
—Adam
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts:
- Django: implement HTTP bearer authentication
- Django: write a custom URL path converter to match given strings
- Django: how to be a teapot
Tags: django