The autologin endpoint is, by design, an admin backdoor. The package's defaults make sure that backdoor never opens unintentionally — and never opens in production.
settings.DJANGO_DEV_HELPERS["enabled"] = False→ always off, even if env says otherwise. A teammate who never wants the helpers can set this insettings.local.pyand forget about it.settings.DJANGO_DEV_HELPERS["enabled"] = True→ always on.DJANGO_DEV_HELPERS_ENABLED=1env var → on, when settings hasn't decided.- None of the above → off.
When the package is active and settings.DEBUG=False and the process
is a serving HTTP runner (runserver / WSGI / ASGI), AppConfig.ready()
raises ImproperlyConfigured. Django doesn't start. There is no override.
Detection is fail-closed: management commands like migrate, test,
shell, dbshell, collectstatic, dev_helpers_* are recognised as
non-serving and don't trigger the check. Anything else —
gunicorn, uwsgi, daphne, uvicorn, custom WSGI runners, unknown
commands — is treated as serving. This biases towards false-positives in
unusual contexts (better than letting a backdoor leak).
To extend the safe list for a project-specific command:
DJANGO_DEV_HELPERS = {
"safety": {"non_serving_commands": ["my_long_running_worker"]},
}To force the check to run inside an otherwise-safe command (CI pre-flight
that asserts a DEBUG=False profile has enabled=False):
DJANGO_DEV_HELPERS_FORCE_SAFETY_CHECK=1 python manage.py checkThe variable name is deliberate: it forces the check to execute, not to pass.
Even with enabled=True and DEBUG=True, the autologin view still:
- accepts only
GET(POST/PUT/DELETE → 405), - requires
DEV_HELPERS_AUTOLOGIN_TOKENto be set, - compares it timing-safely (
hmac.compare_digest), - restricts the request hostname to
localhost,127.0.0.1,[::1]plus any explicit globs inautologin.allowed_hosts, - returns 404 (not 401/403) on every refusal — the URL appears to not exist.
secrets.token_urlsafe(32) — 256 bits, URL-safe. Generated by AppConfig
at runserver start when DEV_HELPERS_AUTOLOGIN_TOKEN isn't already set,
written to .dev_helpers_token (mode 0600).
The token persists in os.environ so Django's autoreload child process
inherits it; on full process exit a fresh token is minted next time.
autologin_urlpatterns() evaluates is_active() and autologin.enabled
at URLconf import time. Django caches the URLconf for the lifetime of
the process. If you flip the flag at runtime, the URL pattern doesn't
appear/disappear until you restart Django.
The view itself still calls cfg.refuse_if_inactive() at request time, so
toggling the flag still 404s the endpoint — the URL just remains routed
until restart.
When autologin.middleware_autoinstall=True (the default), the package
appends django_dev_helpers.middleware.AutologinMiddleware to
settings.MIDDLEWARE during AppConfig.ready(). The middleware checks
request.path against the configured autologin path on every request; for
non-matching paths it calls get_response(request) and exits. For the
matching path it delegates to the view, which performs the same set of
checks documented above (token compare, host allowlist, user lookup,
inactive guard).
The middleware's __init__ raises ImproperlyConfigured when
settings.DEBUG=False. This is a fail-loud-and-early defense: even if
someone copies the dev MIDDLEWARE list into a production-style
configuration, the process refuses to start rather than expose the
token-gated login backdoor. To opt out of the auto-install entirely, set
autologin.middleware_autoinstall=False and either wire
autologin_urlpatterns() in your URLconf or add the middleware manually.
The middleware also handles three convenience query-string toggles
(?__autologin__=tmp_off|logout|log_in). The token is not required
for these: the trust signal is the same host allowlist
(refuse_if_unsafe_host) that gates the autologin URL itself. The
rationale: an attacker outside the allowlist cannot reach the toggles in
the first place; an attacker on localhost can already read
.dev_helpers_token and use the URL flow. The toggles are convenience,
not a new attack surface. Disable them with
autologin.query_param = None (or "") if your threat model is stricter
— for example if you have other users on the localhost loopback or share
a tmux session.
These don't block startup, just emit RuntimeWarning:
ALLOWED_HOSTScontaining entries other thanlocalhost,127.0.0.1,*(your dev box might be configured for staging-via-/etc/hosts; worth flagging).SECRET_KEYlooks production-like (length ≥ 50 and nodjango-insecure-prefix).
- It never logs the token to stdout.
- It never reads request bodies for autologin — token is query-string-only, GET-only.
- It never persists a session or cookie outside of what Django's
login()does. The autologin flow is one round-trip.