Django: Add a .well-known URL

The Owls Know Well

The /.well-known/ URL path prefix is a reserved name space for serving particular static files used by other systems that might interact with your site. It was established by RFC 5785 and updated in RFC 8615. The IANA maintains a list of possible files in the Well Known URIs registry, but others are in wide use without official registeration (yet?).

Some examples:

To add such a URL to a Django application, you have a couple of options.

You could serve it from a web server outside your application, such as nginx. The downside of this approach is that if you move your application to a different web server, you’ll need to redo that configuration. Also, you might be tracking your application code in Git, but not your web server configuration, and it’s best to track changes to your /.well-known/ files, so you don’t lose them.

Instead, it’s better to serve it as a normal URL from within Django. It becomes another view that you can test and update over time.

Let’s look at serving a /.well-known/security.txt URL, which needs only a plain text response.

Writing a View

You can generate a security.txt file for you site with the form at securitytxt.org. A minimum file containts only a method of contact for security reports. Let’s use this example one line file:

Contact: mailto:security@example.com

To serve this from Django, we’d first need to add a new view in our “core” app:

from django.http import HttpResponse
from django.views.decorators.http import require_GET


@require_GET
def security_txt(request):
    lines = [
        "Contact: mailto:security@example.com",
    ]
    return HttpResponse("\n".join(lines), content_type="text/plain")

We’re using Django’s require_GET decorator here to restrict to only GET requests. Class-based views already do this, but we need to think about it ourselves for function-based views.

We generate the security.txt content inside Python, by combining a list of lines using str.join().

Second, we’d need to add a urlconf entry:

from django.urls import path
from core.views import security_txt

urlpatterns = [
    # ...
    path(".well-known/security.txt", security_txt),
]

We’d want to check this works by running manage.py runserver and visiting the URL, for example http://localhost:8000/.well-known/security.txt.

This approach generates the file from a Python list. Doing this allows us some flexibility, for example we could dynamically generate the contact line from a list of staff users’ emails. If you don’t need this flexibility, you could also store the contents of security.txt in a file. I wrote about this approach previously, when covering the similar robots.txt URL in How to add a robots.txt to your Django site.

Testing

As we saw above, one of the advantages of serving .well-known URLs from Django is that we can add automated tests. These tests will guard against accidental breakage of the code, or removal of the URL.

We can add some basic tests for our security.txt URL in a file like core/tests/test_views.py:

from http import HTTPStatus

from django.test import TestCase


class SecurityTxtTests(TestCase):
    def test_get(self):
        response = self.client.get("/.well-known/security.txt")

        self.assertEqual(response.status_code, HTTPStatus.OK)
        self.assertEqual(response["content-type"], "text/plain")
        content = response.content.decode()
        self.assertTrue(content.startswith("Contact: mailto:security@example.com"))

    def test_post_disallowed(self):
        response = self.client.post("/.well-known/security.txt")

        self.assertEqual(HTTPStatus.METHOD_NOT_ALLOWED, response.status_code)

We can run these tests with python manage.py test core.tests.test_views. It’s also a good idea when adding new tests to check that they are being run, by making them fail. We could do this here by commenting out the entry in the URL conf.

Other .well-known URLs

You can adapt this approach to serve any .well-known URL. If you need to serve JSON, such as for .well-known/apple-app-site-association, Django’s JsonResponse class provides a handy shortcut to do this.

Fin

May your site be well known,

—Adam


Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

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

Related posts:

Tags: