django-altcha-widget
A Django form field and widget for ALTCHA, the privacy-friendly proof-of-work CAPTCHA.
It runs fully self-hosted: the ALTCHA JavaScript is vendored into the
package, and the challenge is generated by your own server. No request ever
reaches an external service, and there is no npm install, no bundler
configuration and no CDN. It is secure by default, with built-in
protection against replay attacks ensuring each challenge is only ever
validated once, and it works under a strict Content-Security-Policy — no
'unsafe-inline' styles, no blob: workers — with nothing to configure.
Requires Python 3.12+ and Django 6.0+.
Contents
- Installation
- Usage
- Configuration Options
- Content Security Policy (CSP)
- Replay Attack Protection
- Settings
- Logging
- Contributing
- License
Installation
-
Install the package:
pip install django-altcha-widget
-
Add to
INSTALLED_APPSin your project'ssettings.py:INSTALLED_APPS = [ # Other installed apps "django_altcha_widget", ]
-
Set your secret HMAC key, used to sign ALTCHA challenges. Treat it like a password:
ALTCHA_HMAC_KEY = "5f4dcc3b5aa765d61d8327deb882cf992b95990a9151374abd8ff8c5a7a0fe08"
[!NOTE] Generate one with
python -c "import secrets; print(secrets.token_hex(64))" -
Collect the static files, as you would for any Django app:
python manage.py collectstatic
That is the whole installation. The ALTCHA JavaScript is vendored in the package, so there is no npm dependency, no bundler entry point to edit and no CDN.
Usage
Add the field to your form:
from django import forms
from django_altcha_widget import AltchaField
class MyForm(forms.Form):
captcha = AltchaField()
There is nothing to add to your template: the widget emits the stylesheet and
script tags itself, so a plain {{ form }} is enough. The same tags are also
exposed through Django's
form media, so a
base template already rendering {{ form.media }} gets a second, harmless copy
rather than moving them into the <head>.
Configuration Options
AltchaField accepts the options documented in
Altcha's widget integration guide:
from django import forms
from django_altcha_widget import AltchaField
class MyForm(forms.Form):
captcha = AltchaField(
display="floating", # Enables floating behavior
debug=True, # Enables debug mode (for development)
# Additional options supported by Altcha
)
The options ALTCHA takes as HTML attributes — auto, challenge,
configuration, display, language, theme, type and workers — are
rendered as attributes of the <altcha-widget> element. Every other option is
collected into the JSON-encoded configuration attribute.
Two arguments are refused outright:
name, with aTypeError. It is the name the CAPTCHA value is submitted under, so it always comes from the form field itself.required=False, with aValueError, rather than being honoured or silently ignored: anything submitting the form without the field would pass unchallenged, so an optional CAPTCHA is not a weaker one but no CAPTCHA at all. Leave the field out of the forms that do not need one, or setALTCHA_VERIFICATION_ENABLEDtoFalseto stop verifying.
Register a URL to provide the challenge
By default the challenge is generated by the AltchaField and embedded in the
rendered HTML as JSON, using the challenge option. That same option also
accepts a URL for the widget's JavaScript to fetch instead.
django_altcha_widget ships a ready-to-use view for that. Register it:
from django.urls import path
from django_altcha_widget import AltchaChallengeView
urlpatterns += [
path("altcha/challenge/", AltchaChallengeView.as_view(), name="altcha_challenge"),
]
and point the field at it:
from django.urls import reverse_lazy
from django import forms
from django_altcha_widget import AltchaField
class MyForm(forms.Form):
captcha = AltchaField(
challenge=reverse_lazy("altcha_challenge"),
)
[!NOTE] Challenge generation can be customized when registering the view, for example
AltchaChallengeView.as_view(algorithm="ARGON2ID", cost=3).
Fetching the challenge also keeps it out of the form's HTML, which is what makes
it safe to serve that page from a cache — see
Replay Attack Protection. The view is served
no-store, so the challenge itself is never cached.
Content Security Policy (CSP)
A strict CSP works out of the box. There is nothing to enable:
Content-Security-Policy: script-src 'self'; style-src 'self'; worker-src 'self'
No 'unsafe-inline', no blob:, no third-party origin to allowlist: everything
the widget loads is served from your own static files.
[!NOTE] The
ARGON2IDandSCRYPTalgorithms are implemented in WebAssembly and additionally requirescript-src 'wasm-unsafe-eval'. The defaultPBKDF2/SHA-256algorithm does not.
Replay Attack Protection
django-altcha-widget automatically protects against replay attacks: a
challenge that validates is claimed in a cache, and any later attempt to reuse
it is rejected. The claim is a single atomic cache operation, so submitting one
payload many times at once does not let any copy slip through, and it is held
for as long as the challenge keeps verifying — including challenges issued with
an expiry of their own through AltchaChallengeView.
This is enabled by default and needs no configuration for single-process deployments.
[!IMPORTANT] Replay protection uses Django's
defaultcache backend, which isLocMemCacheunless you configure it otherwise. This in-memory cache is not shared across workers. If you run multiple workers (e.g., with gunicorn or uwsgi), configure a shared cache backend such as Redis or Memcached.
[!IMPORTANT] Do not cache a page that embeds a challenge. By default the challenge is generated afresh on every render and inlined into the form's HTML, so any cache in front of that page serves one challenge to many visitors: the first to submit burns it, and everyone else is rejected as a replay. If the view rendering the form is wrapped in
@cache_page, sits behindUpdateCacheMiddleware, or is cached by a CDN or reverse proxy, either exclude it with@never_cacheor have the widget fetch the challenge from a URL instead — see Register a URL to provide the challenge.AltchaChallengeViewsets the no-store headers for you.
Settings
ALTCHA_HMAC_KEY
Required. The key used to HMAC-sign ALTCHA challenges; it must be kept
secret. Forging a challenge is exactly as hard as guessing it, so keys shorter
than 32 characters are rejected with an ImproperlyConfigured error.
ALTCHA_CACHE_ALIAS
Django cache alias used for replay attack protection. Defaults to "default",
which needs no further configuration if that cache is already a shared backend
(Redis, Memcached, database).
To use a dedicated cache, define one and point to it:
CACHES = {
"altcha": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
ALTCHA_CACHE_ALIAS = "altcha"
Django's database cache
is a simple alternative if you would rather not run Redis or Memcached — use
"BACKEND": "django.core.cache.backends.db.DatabaseCache" with the table name
as LOCATION, then create it with python manage.py createcachetable.
ALTCHA_CHALLENGE_EXPIRE
Challenge expiration duration in milliseconds. Defaults to 20 minutes as per Altcha security recommendations.
ALTCHA_ALGORITHM
Key derivation function used for the Proof-of-Work challenges. Defaults to
"PBKDF2/SHA-256". Supported values are "PBKDF2/SHA-256",
"PBKDF2/SHA-384", "PBKDF2/SHA-512", "SHA-256", "SHA-384", "SHA-512",
"ARGON2ID" and "SCRYPT". See
Altcha's Proof-of-Work documentation
for the trade-offs between them.
[!NOTE]
"ARGON2ID"requires theargon2-cffipackage, installable withpip install 'django-altcha-widget[argon2]'.
ALTCHA_COST
Algorithm-specific cost: the number of iterations for PBKDF2 and SHA, the
time cost for ARGON2ID and SCRYPT. Defaults to 5000, the value recommended
upstream for PBKDF2/SHA-256.
ALTCHA_TRANSLATIONS
The Altcha translations
to load, as a language code or "all" for the combined bundle covering every
language. Defaults to None, which loads none and leaves the widget in English.
Serving a single language is much lighter than the combined bundle — 1.4 KB gzipped instead of 18.2 KB:
ALTCHA_TRANSLATIONS = "fr-fr"
Every per-language file ALTCHA ships is vendored, along with the combined
"all" bundle, so any of those works without installing anything. A trailing
.js is tolerated: "fr-fr" and "fr-fr.js" name the same file.
The four regional bundles ALTCHA also publishes — "africa", "americas",
"asia" and "europe" — are not vendored. Naming one loads nothing under a
plain static files storage, and raises ValueError: Missing staticfiles manifest entry under ManifestStaticFilesStorage. Use "all" or a language code
instead.
ALTCHA_VERIFICATION_ENABLED
Set to False to skip Altcha validation altogether. Defaults to True.
Logging
Logs are emitted through the standard Python logging module under the logger
name django_altcha_widget. Nothing is logged under normal operation; logging
fires only on validation failures and misconfiguration:
- WARNING on invalid or missing CAPTCHA tokens submitted to a form.
- WARNING on replay attempts (a challenge reused after it has already been validated).
- ERROR when
ALTCHA_HMAC_KEYis not configured. - Exception with traceback when verification or payload decoding raises unexpectedly.
Payloads, challenge values, and the HMAC key are never included in log messages.
To see them, add the logger to your project's LOGGING setting:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"django_altcha_widget": {
"handlers": ["console"],
"level": "WARNING",
},
},
}
Set "level": "ERROR" to see only misconfiguration and unexpected failures,
or "level": "DEBUG" to see additional diagnostic messages during development.
Contributing
Issues and pull requests are welcome. See
DEVNOTES.md
for how to set up a development environment and how the test matrix, the
vendored assets and the release pipeline work. Please run just check and
just test before opening a pull request.
License
This project is licensed under the MIT License. See the LICENSE file for details.
It began as a fork of django-altcha, Copyright (c) nexB Inc. and others, also MIT licensed. It is published as a separate package and shares no release history with it; installing both in the same environment is not supported.
The ALTCHA JavaScript library is Copyright (c) 2023-2026 Daniel Regeci, BAU
Software s.r.o., MIT licensed. It is vendored in this package and redistributed
under that license, whose text travels with it at
src/django_altcha_widget/static/django_altcha_widget/altcha/LICENSE.txt. The
exact version and provenance are recorded alongside it in VENDOR.json.
Release files for django-altcha-widget 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_altcha_widget-1.0.0.tar.gz | 168.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_altcha_widget-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 392.9 kB
Release files / django_altcha_widget-1.0.0.tar.gz
| Download URL | django_altcha_widget-1.0.0.tar.gz |
|---|---|
| Size | 168.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
207bb369db16273d4848ce463dc8bc9fd71349e5d077d58c3aa634328d0afce7
|
|
BLAKE2b-256 checksum How to use checksums |
9da8c0576a12b46786ad82538d8ec9f3e0190bb277a8a78eeef85436e83e6f33
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / django_altcha_widget-1.0.0-py3-none-any.whl
| Download URL | django_altcha_widget-1.0.0-py3-none-any.whl |
|---|---|
| Size | 224.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f21668741903aa67bd5e8783f3b741cb05fd6a9b2fff50ff97e6460955ed2a26
|
|
BLAKE2b-256 checksum How to use checksums |
0a312d2f933b605f2f2317d94796102287b873cde467f897269879bc09c20e8b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency log