Skip to main content

django-altcha-widget

PyPI Python versions Django versions Tests License

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

  1. Install the package:

    pip install django-altcha-widget
    
  2. Add to INSTALLED_APPS in your project's settings.py:

    INSTALLED_APPS = [
        # Other installed apps
        "django_altcha_widget",
    ]
    
  3. 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))"

  4. 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 a TypeError. It is the name the CAPTCHA value is submitted under, so it always comes from the form field itself.
  • required=False, with a ValueError, 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 set ALTCHA_VERIFICATION_ENABLED to False to 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 ARGON2ID and SCRYPT algorithms are implemented in WebAssembly and additionally require script-src 'wasm-unsafe-eval'. The default PBKDF2/SHA-256 algorithm 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 default cache backend, which is LocMemCache unless 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 behind UpdateCacheMiddleware, or is cached by a CDN or reverse proxy, either exclude it with @never_cache or have the widget fetch the challenge from a URL instead — see Register a URL to provide the challenge. AltchaChallengeView sets 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 the argon2-cffi package, installable with pip 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_KEY is 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)

Source distribution for django-altcha-widget 1.0.0
File Size Uploaded
django_altcha_widget-1.0.0.tar.gz 168.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-altcha-widget 1.0.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page