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

Django News - Django 6.0 released! - Dec 5th 2025

News

Django 6.0 released

Django 6.0 introduces template partials, a background task framework, Content Security Policy support, and a modern Email API based on EmailMessage.

djangoproject.com

2026 DSF Board Election Results

Jacob Kaplan-Moss, Priya Pahwa, and Ryan Cheley were elected to two year terms, joining continuing directors to form the 2026 DSF Board.

djangoproject.com

Releases

Django security releases issued: 5.2.9, 5.1.15, and 4.2.27

Django issues security releases 5.2.9, 5.1.15, and 4.2.27, addressing a FilteredRelation SQL injection on PostgreSQL and an XML serializer DoS vulnerability.

djangoproject.com

Python 3.14.1 is now available!

Python 3.14.1 is a maintenance release that adds deferred annotation evaluation, free-threaded support, an experimental JIT, and numerous bug fixes affecting Django performance and typing.

python.org

Python 3.13.10 is now available, too, you know!

Python 3.13.10 is released as the tenth maintenance update since 3.13.9, delivering around 300 bug fixes, build improvements, and documentation updates.

python.org

Wagtail 7.2.1

Wagtail 7.2.1 delivers several performance and usability fixes for document and image listings, improves reference index efficiency, and includes minor maintenance updates.

github.com

Python Software Foundation

Python Software Foundation News: Sovereign Tech Agency and PSF Security Partnership

Sovereign Tech Agency funds €86,000 to PSF for CPython archive fuzz testing and PyPI account recovery using OAuth 2.0 OIDC to strengthen supply chain security.

blogspot.com

Djangonaut Space News

We're recruiting admins!

Djangonaut Space seeks volunteer Admins for a one-year, ~5 hours per month role handling socials, fundraising, outreach, and program development; apply by Dec 12.

djangonaut.space

Celebrating Djangonaut Space 2025 Session 5 Achievements

Djangonaut Space Session 5 highlights cross-project contributions, merged PRs, package releases, conference talks, and collaboration that advanced Django and related projects.

djangonaut.space

Djangonaut Space

Ernesto Rico Schmidt, known online as "eigenwijsje," reflects on his Djangonaut Space experience and how starting small, asking for help, and embracing what he didn't realize were his strengths enabled him to begin contributing to Django and Wagtail.

eigenwijsje.dev

Django Fellow Reports

Fellow Report - Natalia

This week was again heavy on security work. The flow of incoming security reports keeps growing, and even when many are medium to low impact, keeping up is starting to get tough: most of my time went into security report triage, patch review, and planning so we can (try to) stay on top of things. I also pushed forward the Django 6.0 RC1 release and wrapped up a few small docs and tooling updates.

djangoproject.com

Fellow Report - Jacob

Short week for US Thanksgiving. Advanced some fixes for the next patch release of 5.2.

djangoproject.com

Wagtail CMS News

Our package maintainers were productive in 2025 (but finding all their names was a bit hard)

Analysis shows the Wagtail ecosystem produced 714 package releases in 2025, with maintainers compiled using PyPI BigQuery and PyPI page scraping due to incomplete metadata.

wagtail.org

Articles

Django: What's new in 6.0

Adam Johnson highlights Django 6.0's major updates, including template partials, a built-in task framework, CSP support, and a modernized email API, and shows how the release improves the developer experience, security, and scalability.

adamj.eu

A first look at Django's new background tasks

Django 6.0 introduces a built-in background tasks framework in django.tasks. But don't expect to phase out Celery, Huey or other preferred solutions just yet.

roam.be

Safe Django database migrations on Coolify

Use two phase deploys with expand then contract nonbreaking migrations to avoid runtime errors during rolling updates when changing Django database schema.

loopwerk.io

Build self-hosted AI Agent with Ollama, Pydantic AI and Django Ninja

Create a self-hosted AI agent using Ollama and Pydantic AI integrated into Django with Django Ninja APIs, JWT authentication, and tool-based interactions.

scribe.rip

Django Third-party App Ecosystem

Django Steering Council launched a curated Ecosystem Page to highlight well-supported third-party apps and resources, encouraging regular exploration to avoid reinventing features.

frankwiles.com

YouTube embeds fail with a 153 error

This 153 error plagued DjangoTV and most websites with YouTube videos for months.

simonwillison.net

Events

Djangon Con EU 2026 Sponsorship

Sponsorships are now OPEN. There are a wide range of packages, from Bronze to Diamond, along with unique add-on opportunities like sponsoring coffee breaks, meals, and more.

djangocon.eu

Django News Jobs

Here are three exciting new openings for Django- and Python-focused engineers:

Python/Django Senior Application Security Engineer at Energy Solutions 🆕

Python / Django Developer at Client of Foxley Talent 🆕

Staff Software Engineer at Bluebird Kids Health 🆕

Django Newsletter

Django Forum

Preparations for 2026 DSF Board elections - Django Internals

The Django Software Foundation is planning the 2026 Board elections and actively seeking community feedback and volunteers to improve the process. Join the forum discussion and get involved.

djangoproject.com

Projects

usmanhalalit/DjangoRealtime

Realtime browser events, SSE for Django + PostgreSQL.

github.com

Django-LiveView/liveview

Django LiveView: Framework for creating Realtime SPAs using HTML over the Wire technology.

github.com


This RSS feed is published on https://django-news.com/. You can also subscribe via email.

05 Dec 2025 5:00pm GMT