Authentication plugin for Pyxle: argon2id sessions, password reset and email verification flows, RBAC, scoped API tokens, and request guards.
Project description
pyxle-auth
Django-grade authentication for Pyxle apps:
sessions, password reset and email verification flows, roles and
permissions, API tokens, and one-line request guards. Built on
pyxle-db,
so the same code runs on SQLite, PostgreSQL, and MySQL. (Caveat: every query is portable across all three, but the shipped schema files target SQLite and PostgreSQL; MySQL needs a dialect-override migration — 0001-pyxle-auth-core.mysql.sql — because MySQL requires key lengths on TEXT keys. On the roadmap; contributions welcome.)
- Sessions — argon2id-hashed passwords, server-side sessions with
sliding expiry and an absolute cap,
HttpOnly; Secure; SameSite=Laxcookies. - Password reset & email verification — single-use, purpose-scoped, expiring tokens. The library never sends email; your app delivers the link through its own mailer.
- RBAC — roles, permissions, per-user grants, and
require_permission_*guards. - API tokens — long-lived
pyxle_pat_personal access tokens with scopes, per-user caps, and revocation, for CLIs and CI. - Guards —
require_user_page(request)and friends protect a loader or action in one line. - Rate limits — database-backed fixed-window buckets on sign-in, sign-up, and reset requests; they survive process restarts.
Install
pip install pyxle-auth
Quickstart
List pyxle-db before pyxle-auth in pyxle.config.json — the
auth services run on the database that plugin opens:
{
"plugins": [
"pyxle-db",
"pyxle-auth"
]
}
That's the whole wire-up. At startup the plugin applies its bundled migrations (idempotent, checksum-tracked) and registers the services listed below.
Protect a page with a guard in its @server loader:
# pages/dashboard.pyxl — Python section
from pyxle.runtime import server
from pyxle_auth import require_user_page
@server
async def load(request):
user = await require_user_page(request) # 401 → error boundary when signed out
return {"email": user.email}
Sign-in needs to put a Set-Cookie header on the response, so it lives
in an API route (actions return plain JSON
payloads and can't attach cookies):
# pages/api/sign_in.py
from starlette.requests import Request
from starlette.responses import JSONResponse
from pyxle_auth import AuthError, RateLimited, get_auth_service
async def endpoint(request: Request) -> JSONResponse:
body = await request.json()
auth = get_auth_service()
try:
user, cookie = await auth.sign_in(
email=body["email"],
password=body["password"],
ip=request.client.host,
user_agent=request.headers.get("user-agent", ""),
)
except RateLimited as exc:
return JSONResponse(
{"ok": False, "error": str(exc)},
status_code=429,
headers={"Retry-After": str(exc.retry_after_seconds)},
)
except AuthError as exc:
# InvalidCredentials and friends share one deliberately vague
# message — don't replace it with something more "helpful".
return JSONResponse({"ok": False, "error": str(exc)}, status_code=401)
response = JSONResponse({"ok": True, "userId": user.id})
response.set_cookie(**cookie.kwargs())
return response
sign_up has the same shape. sign_out(cookie_value=...) returns a
cookie that clears the browser's copy — set it the same way.
Bring your own mailer
pyxle-auth never sends email. Flows that need delivery return a raw, single-use token exactly once; your app puts it in a link and hands it to whatever mailer it already uses:
# pages/api/forgot_password.py
async def endpoint(request: Request) -> JSONResponse:
body = await request.json()
auth = get_auth_service()
result = await auth.request_password_reset(
email=body["email"], ip=request.client.host
)
if result is not None:
user, token = result
await my_mailer.send(
to=user.email,
subject="Reset your password",
body=f"https://example.com/reset?token={token}",
)
# Same response whether the account exists or not — this endpoint
# must not be usable to probe for accounts.
return JSONResponse({"ok": True, "message": "Check your inbox."})
The user completes the flow with
await auth.reset_password(raw_token=token, new_password=...), which
burns the token and revokes every session. Email verification mirrors
the pattern: request_email_verification(user_id=...) returns a token,
confirm_email(raw_token=...) redeems it. Both raise InvalidToken
for anything stale, used, unknown, or wrong-purpose —
indistinguishably.
For your own flows (invite links, magic links), the same machinery is
registered as auth.tokens: issue with a custom purpose, consume it
once, never store the raw value.
Bring your own database
pyxle-auth binds to the db.database plugin service, not to the
pyxle-db package. The reference provider is pyxle-db, but any plugin (or
test fixture) that registers an object satisfying
pyxle_db.DatabaseLike works — an adapter over SQLAlchemy's async
engine, a bespoke driver wrapper, an in-memory fake.
The full contract a replacement must honour:
- Surface — the five members of
pyxle_db.DatabaseLike:execute,fetchone,fetchall, an async-context-managertransaction()(yielding the same query surface), and adialectproperty returning apyxle_db.Dialect. SQL arrives in canonical qmark style (?placeholders); rows go back aspyxle_db.Row. - Errors — unique-constraint violations must raise
pyxle_db.IntegrityError. pyxle-auth converts it into domain behaviour (AccountExistson duplicate sign-up, idempotent role grants); raise your driver's own error type and those paths break. - Dialect —
dialect.namedrives portable DDL.sqlite,postgresql, andmysqlhave live-tested paths; any other name falls back to the SQLite/PostgreSQL-flavoured DDL (right for PostgreSQL-compatible engines, wrong for e.g. MSSQL). - Datetimes — reads return timezone-aware UTC; binds accept naive (assumed UTC) or aware (converted) datetimes.
tests/test_database_contract.py runs the entire auth lifecycle against
a wrapper that exposes only this surface — it is both the executable
specification and a template for writing your own adapter.
Security properties
- Password hashing — argon2id,
t=3, m=64 MiB, p=2by default (~300 ms on a 2020-era laptop), tunable via settings. Hashes are transparently upgraded on sign-in when parameters change. - Nothing secret at rest — session cookies, reset/verification tokens, and API tokens all store only the SHA-256 of the secret. A leaked database cannot resurrect a session or replay a reset link.
- Enumeration resistance — sign-in failures share one message and run a dummy argon2 verify on unknown emails so timing stays flat; password-reset requests do token-shaped work and return the same shape whether the account exists or not; token redemption never says why it failed.
- Single-use tokens — redemption burns the token atomically, so two racing requests can't both succeed, and requesting a new reset link invalidates earlier unused ones.
- Rate limits — sign-in is capped per IP and per email (10/hour each), sign-up per IP (5/hour), reset requests per email and per IP (3/hour). Buckets live in the database and survive restarts.
- Session lifecycle — sliding expiry (30 days) under an absolute
cap (90 days); password change and password reset revoke every
session;
list_sessions/revoke_sessionpower a "your devices" screen. - Cookie posture —
HttpOnly,Secure,SameSite=Laxby default. Strict mode (the default) refuses to start withcookie_secure=False.
Plugin services
| Service | Type | Use it for |
|---|---|---|
auth.service |
AuthService |
Sign-up/in/out, sessions, password change/reset, email verification |
auth.rbac |
RoleService |
Define roles, grant them, check permissions |
auth.tokens |
TokenService |
Custom single-use token flows (invites, magic links) |
auth.api_tokens |
ApiTokenService |
pyxle_pat_ personal access tokens |
auth.settings |
AuthSettings |
The resolved configuration (cookie name, TTLs, …) |
Reach them with ctx.require(...), pyxle.plugins.plugin(...), or the
typed helpers get_auth_service() / get_auth_settings().
Guards resolve auth.service / auth.rbac automatically; pass
service= / rbac= explicitly in tests. For API routes authenticating
with personal access tokens, pair bearer_token(request) with
ApiTokenService.resolve(raw_token=..., required_scope=...).
Settings
Precedence: plugin settings in pyxle.config.json >
PYXLE_AUTH_* environment variables > defaults.
| Config key | Environment variable | Default | Meaning |
|---|---|---|---|
argonTimeCost |
PYXLE_AUTH_ARGON_T |
3 |
Argon2 time cost |
argonMemoryKib |
PYXLE_AUTH_ARGON_M |
65536 |
Argon2 memory (KiB) |
argonParallelism |
PYXLE_AUTH_ARGON_P |
2 |
Argon2 parallelism |
passwordMinLength |
PYXLE_AUTH_PW_MIN |
8 |
Reject shorter passwords |
passwordMaxLength |
— | 1024 |
Reject pathological inputs |
sessionTtlSeconds |
PYXLE_AUTH_SESSION_TTL |
2592000 (30 d) |
Sliding session lifetime |
sessionAbsoluteMaxSeconds |
PYXLE_AUTH_SESSION_ABS_MAX |
7776000 (90 d) |
Hard cap from creation |
cookieName |
PYXLE_AUTH_COOKIE_NAME |
pyxle_session |
Session cookie name |
cookieSecure |
PYXLE_AUTH_COOKIE_SECURE |
true |
Secure cookie flag |
cookieSameSite |
PYXLE_AUTH_COOKIE_SAMESITE |
Lax |
Lax / Strict / None |
cookieDomain |
PYXLE_AUTH_COOKIE_DOMAIN |
unset | Share across subdomains |
cookiePath |
— | / |
Cookie path |
passwordResetTtlSeconds |
PYXLE_AUTH_PASSWORD_RESET_TTL_SECONDS |
1800 (30 min) |
Reset-token lifetime |
emailVerifyTtlSeconds |
PYXLE_AUTH_EMAIL_VERIFY_TTL_SECONDS |
86400 (24 h) |
Verify-token lifetime |
rateLimitSignInPerHour |
PYXLE_AUTH_RL_SIGN_IN_PER_HOUR |
10 |
Per IP and per email |
rateLimitSignUpPerHour |
PYXLE_AUTH_RL_SIGN_UP_PER_HOUR |
5 |
Per IP |
rateLimitPasswordResetPerHour |
PYXLE_AUTH_RATE_LIMIT_PASSWORD_RESET_PER_HOUR |
3 |
Per email and per IP |
requireEmailVerified |
PYXLE_AUTH_REQUIRE_VERIFIED |
false |
Gate sign-in on verification |
strict |
— | true |
Enforce cookieSecure=true; set false for HTTP dev servers |
Outside the plugin, load the same configuration with
AuthSettings.from_env(), and use AuthSettings(...).for_tests() in
test suites — it drops argon costs and TTLs so suites stay fast.
Schema
The plugin owns its tables (users, sessions, auth_tokens,
api_tokens, roles, user_roles, ratelimit_buckets): bundled
migrations are applied through pyxle_db.Migrator at startup, followed
by each service's idempotent ensure_schema(). Repeated startups are
no-ops. The SQL is portable qmark style throughout, so the plugin works
on every pyxle-db backend without per-database configuration.
Roadmap
Honest status — these are not implemented yet:
- OAuth / OIDC sign-in (Google, GitHub, generic OIDC)
- Multi-factor authentication (TOTP, WebAuthn)
If you need them today, the building blocks (sessions, TokenService,
guards) compose underneath whatever you bring; contributions are
welcome.
License
MIT.
Project details
Release history Release notifications | RSS feed
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 pyxle_auth-0.2.0.tar.gz.
File metadata
- Download URL: pyxle_auth-0.2.0.tar.gz
- Upload date:
- Size: 61.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
371004031089c64eb9e01bfd75e24d7bcb86a4606a66befcc3257ea6625a0dc6
|
|
| MD5 |
0d29aa5bff65f1f902575ca4f783c69f
|
|
| BLAKE2b-256 |
250376bfecb7901e8104e8c4df0d805795b155952a3ae406334e1b413c782490
|
Provenance
The following attestation bundles were made for pyxle_auth-0.2.0.tar.gz:
Publisher:
publish.yml on pyxle-dev/pyxle-plugins
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyxle_auth-0.2.0.tar.gz -
Subject digest:
371004031089c64eb9e01bfd75e24d7bcb86a4606a66befcc3257ea6625a0dc6 - Sigstore transparency entry: 1800600717
- Sigstore integration time:
-
Permalink:
pyxle-dev/pyxle-plugins@e6cc4b6a68bd1bcba2885c0f48a61e02a6f5a24d -
Branch / Tag:
refs/tags/pyxle-auth-v0.2.0 - Owner: https://github.com/pyxle-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e6cc4b6a68bd1bcba2885c0f48a61e02a6f5a24d -
Trigger Event:
release
-
Statement type:
File details
Details for the file pyxle_auth-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pyxle_auth-0.2.0-py3-none-any.whl
- Upload date:
- Size: 45.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f78b7c4b6227b8cb11a3999e33906bb7ce2036b316e86ba1f99a8968d4beaae
|
|
| MD5 |
51d7a5a52b6142760712bc5e32b1880f
|
|
| BLAKE2b-256 |
1d2597ad0e575d87e8536725acf2c3dd8ded6330c44395303ba73e6959468e1a
|
Provenance
The following attestation bundles were made for pyxle_auth-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on pyxle-dev/pyxle-plugins
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyxle_auth-0.2.0-py3-none-any.whl -
Subject digest:
4f78b7c4b6227b8cb11a3999e33906bb7ce2036b316e86ba1f99a8968d4beaae - Sigstore transparency entry: 1800601401
- Sigstore integration time:
-
Permalink:
pyxle-dev/pyxle-plugins@e6cc4b6a68bd1bcba2885c0f48a61e02a6f5a24d -
Branch / Tag:
refs/tags/pyxle-auth-v0.2.0 - Owner: https://github.com/pyxle-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e6cc4b6a68bd1bcba2885c0f48a61e02a6f5a24d -
Trigger Event:
release
-
Statement type: