stapel-auth
User authentication and account access: sign-up and sign-in with email or phone one-time codes, passwords, social (OAuth) accounts, corporate SSO (SAML/OIDC), magic links, QR hand-off and passkeys; guest (anonymous) access; two-factor authentication and step-up verification; session and device management with a security audit trail.
Part of the Stapel framework — composable Django apps that deploy as a monolith or as microservices without changing module code.
Install
pip install stapel-auth
At a glance
| Fact | Value |
|---|---|
| Version | 0.31.1 |
| Python | >=3.11 (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 122 |
| Config axes | 29 |
| Usage surface | 18 |
| Extension points | 6 |
| Error codes | 139 |
| Documented flows | 4 |
| Fleet dependencies | stapel-core · stapel-gdpr (optional) · stapel-notifications (optional) |
Documentation
Flows: English · Русский · Errors: English · Español · Русский · OpenAPI · capabilities.json · llms.txt (for agents)
What this is
Authentication is the part of a product that everyone builds and nobody wants
to own: a dozen sign-in methods, each with its own rate limits, lockouts,
notification rules and recovery paths, plus the security surface underneath
them — sessions, devices, audit, step-up. stapel-auth is that whole area as
one installable Django app, with every method behind a settings flag so a
product ships only the ones it wants.
The shape to keep in mind: sign-in methods are axes, not forks. Email OTP,
phone OTP, password (+ TOTP), OAuth, enterprise SSO, magic link, QR hand-off,
passkeys and guest access are each one AUTH_* flag. Turning a flag off
unmounts its endpoints and removes it from the capabilities response the
frontend reads — so the login screen changes with the setting, not with a
frontend release. GET /capabilities/ is the contract for that: availability,
placement, interaction and icon per method, plus OTP code length, TTL and
resend cooldown, so no client hardcodes a number this module owns.
Quick start
# settings.py
INSTALLED_APPS = [
# ...
"stapel_auth",
]
STAPEL_AUTH = {
"AUTH_EMAIL": True, # email OTP sign-in
"AUTH_PASSWORD_LOGIN": True, # password (+ TOTP step-up when enrolled)
"AUTH_ANONYMOUS": False, # no guest accounts
}
# urls.py
path("auth/", include("stapel_auth.urls")),
python manage.py migrate
Behind a reverse proxy, tell the framework which header carries the client
IP — rate limits, lockouts and the IP in every audit row are keyed on it, and
by default only REMOTE_ADDR (i.e. the proxy) is trusted:
# ONLY if the edge overwrites this header on every request
# (nginx: proxy_set_header X-Real-IP $remote_addr)
STAPEL_NETINTEL = {"TRUSTED_PROXY_HEADER": "HTTP_X_REAL_IP"}
manage.py check says so too: stapel_auth.W005 when a proxy is declared but
no header is, stapel_auth.W006 when the named header is one proxies usually
append to (X-Forwarded-For), where the first element is whatever the caller
wrote.
If your frontend posts provider access tokens to POST /oauth/login/ rather
than using the redirect flow, pin which OAuth clients may vouch for an
identity — a token is a bearer credential for the app it was minted for, so an
unpinned endpoint accepts one minted for somebody else's app:
STAPEL_AUTH = {
"OAUTH_ACCEPTED_AUDIENCES": {
# a LIST — Google issues one client ID per platform
"google": ["<web>.apps.googleusercontent.com",
"<ios>.apps.googleusercontent.com"],
},
}
Google, Facebook and GitHub can prove a token's audience; Zoom and the
not-yet-implemented providers cannot, so they refuse that endpoint and keep
working through the redirect flow. W007/E008/W009/W010 report which
case each configured provider is in.
Every configuration axis, its default and the operations it gates are listed
in docs/capabilities.json — the same document the
table above is generated from, and the one an agent reads before writing code
against this module.
Step-up verification
Any endpoint in any module can demand a fresh proof of identity by decorating
itself with @requires_verification (from stapel_core.verification). This
module registers the factors that satisfy it — otp_email, otp_phone,
totp, passkey — and hosts the challenge endpoints.
The factors are interchangeable by design: a challenge names a scope and the
factors currently available to that user, and any of them closes it. A
client implements the cycle once (403 with a challenge envelope → pick a
factor → initiate → complete → repeat the original request) and reuses it for
every protected endpoint in the product, forever. The reference walkthrough is
the auth.step_up_verification flow.
Sessions, devices and recovery
Sessions are JWT (cookie plus a token pair) with a tracked UserSession per
device, so "sign out everywhere" and "revoke this device" are real operations
rather than a token TTL. Suspicious sessions (new device, unexpected IP) are
detected, notified and revocable from the notification itself.
Authenticator changes — email, phone or TOTP — run through one model and one set of tasks, in two speeds: instant, when the user can prove control of the current authenticator, and delayed, when they cannot. There is no third speed: a code sent to a new address can set a first email or phone, never replace a verified one, so a stolen session cannot quietly move the recovery address out of the owner's reach. The delayed path is the one that matters after a lost phone: it notifies the verified contact on day 1, 7 and 13 and completes on day 14, which gives an attacker who has the inbox but not the device two weeks of loud warnings and the real owner two weeks to cancel.
Enterprise SSO
SAML SP and OIDC RP, configured per organization in the database rather than
in settings — a tenant onboards without a deploy. Users provisioned by an org
admin land in the auth.first_login flow: the first password login returns a
short-lived challenge instead of a session, routing to a forced password change
and/or MFA enrolment before anything else is reachable.
Bus events
Emitted through stapel_core.comm (transactional outbox — the event leaves if
and only if your transaction commits):
| Event | Payload | When |
|---|---|---|
user.session_created |
schema | A user authenticated and a session was created |
user.session_revoked |
schema | A session was revoked (logout or admin action) |
Extension points
Providers, models and policies are replaced by dotted path, never by fork —
additional OAuth providers, a custom re-registration model, serializer and
permission seams. MODULE.md is the full agent-facing map;
docs/capabilities.json carries the machine-readable list.
Development
pip install -e . && pip install pytest pytest-django pytest-cov ruff
./setup-hooks.sh
pytest tests/
License
MIT — see LICENSE.
This page is assembled by stapel-readme from docs/readme.md plus the contract artifacts in docs/. Edit the prose in docs/readme.md; the badges, facts and links above and below it are generated — do not hand-edit README.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stapel_auth-0.31.1.tar.gz.
File metadata
- Download URL: stapel_auth-0.31.1.tar.gz
- Upload date:
- Size: 519.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
710cdc0ffa2943e1a6802cdd5a13d531434ade02d3113c3e651f759b5c5c28ed
|
|
| MD5 |
f6f781cf3ad4a35b424f3bdcae6ac922
|
|
| BLAKE2b-256 |
6284671157c8df2e9088ecdd938ace7b16517304072ef2814670d49304d5d767
|
Provenance
The following attestation bundles were made for stapel_auth-0.31.1.tar.gz:
Publisher:
publish.yml on usestapel/stapel-auth
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_auth-0.31.1.tar.gz -
Subject digest:
710cdc0ffa2943e1a6802cdd5a13d531434ade02d3113c3e651f759b5c5c28ed - Sigstore transparency entry: 2646077222
- Sigstore integration time:
-
Permalink:
usestapel/stapel-auth@c45326601f9e5fdee42b601c72593da30b201e30 -
Branch / Tag:
refs/tags/v0.31.1 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c45326601f9e5fdee42b601c72593da30b201e30 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stapel_auth-0.31.1-py3-none-any.whl.
File metadata
- Download URL: stapel_auth-0.31.1-py3-none-any.whl
- Upload date:
- Size: 345.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c4db6f93b33ef44654f82680faddf6d4f4aa9c343ee31241a2c510130775f41
|
|
| MD5 |
ca3230f0a1758a062b6f219dbb2959ff
|
|
| BLAKE2b-256 |
323828dc492858b1b32163cc31dcdaac6463b5085ccec53ea8892c5ac47888ba
|
Provenance
The following attestation bundles were made for stapel_auth-0.31.1-py3-none-any.whl:
Publisher:
publish.yml on usestapel/stapel-auth
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_auth-0.31.1-py3-none-any.whl -
Subject digest:
0c4db6f93b33ef44654f82680faddf6d4f4aa9c343ee31241a2c510130775f41 - Sigstore transparency entry: 2646077279
- Sigstore integration time:
-
Permalink:
usestapel/stapel-auth@c45326601f9e5fdee42b601c72593da30b201e30 -
Branch / Tag:
refs/tags/v0.31.1 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c45326601f9e5fdee42b601c72593da30b201e30 -
Trigger Event:
push
-
Statement type: