Django: Add a .well-known URL
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:
/.well-known/acme-challenge
is used for acknowledging HTTPS certificate challenges through Automatic Certificate Management Environment (ACME). This is used by popular free certificate authority Lets Encrypt./.well-known/apple-app-site-association
allows links to your domain to open an iOS app./.well-known/assetlinks.json
allows links to your domain to open an app on Android phones./.well-known/security.txt
is a proposed standard for hosting your site’s security policy that others should use when they find vulnerabilities in your site.
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.
Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.
One summary email a week, no spam, I pinky promise.
Related posts:
- How to add a robots.txt to your Django site
- How to Unit Test a Django Form
- Getting a Django Application to 100% Test Coverage
Tags: django