11 Dec 2025

feedDjango community aggregator: Community blog posts

Django 6.0 - Natalia Bidart

Sponsor

This episode was brought to you by HackSoft, your development partner beyond code. From custom software development to consulting, team augmentation, or opening an office in Bulgaria, they're ready to take your Django project to the next level!

11 Dec 2025 11:05pm GMT

08 Dec 2025

feedDjango community aggregator: Community blog posts

Django: implement HTTP basic authentication

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:

  1. Clients may provide an authorization request header that contains a credential.
  2. 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:

Firefox HTTP Basic authentication dialog

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.

Extract a decorator to curtail constant copying

If you have multiple views that need authentication, don't copy authentication code between them. Instead, extract the authentication logic into a view decorator, like:

import base64
import functools
import os
import secrets
from http import HTTPStatus

from django.shortcuts import render

SECRET_STUFF_USERNAME = os.environ.get("SECRET_STUFF_USERNAME", "")
SECRET_STUFF_PASSWORD = os.environ.get("SECRET_STUFF_PASSWORD", "")


def basic_auth(view_func):
    @functools.wraps(view_func)
    def _wrapped_view(request, *args, **kwargs):
        authorization = request.headers.get("authorization", "")

        if not SECRET_STUFF_USERNAME or not SECRET_STUFF_PASSWORD:
            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 view_func(request, *args, **kwargs)

    return _wrapped_view


def _unauthorized(request):
    response = render(
        request,
        "unauthorized.html",
        status=HTTPStatus.UNAUTHORIZED,
    )
    response.headers["www-authenticate"] = 'Basic realm="Secret area!"'
    return response


@basic_auth
def secret_recommendation(request):
    return render("secret_recommendation.html", request)


@basic_auth
def secret_info(request):
    return render("secret_info.html", request)

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

08 Dec 2025 6:00am GMT

05 Dec 2025

feedDjango community aggregator: Community blog posts

Weeknotes (2025 week 49)

Weeknotes (2025 week 49)

I seem to be publishing weeknotes monthly, so I'm now thinking about renaming the category :-)

Mosparo

I have started using a self-hosted mosparo instance for my captcha needs. It's nicer than Google reCAPTCHA. Also, not sending data to Google and not training AI models on traffic signs feels better.

Fixes for the YouTube 153 error

Simon Willison published a nice writeup about YouTube embeds failing with a 153 error. We have also encountered this problem in the wild and fixed the feincms3 embedding code to also set the required referrerpolicy attribute.

Updated packages since 2025-11-04

05 Dec 2025 6:00pm GMT