Skip to main content

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=Lax cookies.
  • 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.
  • Guardsrequire_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:

  1. Surface — the five members of pyxle_db.DatabaseLike: execute, fetchone, fetchall, an async-context-manager transaction() (yielding the same query surface), and a dialect property returning a pyxle_db.Dialect. SQL arrives in canonical qmark style (? placeholders); rows go back as pyxle_db.Row.
  2. Errors — unique-constraint violations must raise pyxle_db.IntegrityError. pyxle-auth converts it into domain behaviour (AccountExists on duplicate sign-up, idempotent role grants); raise your driver's own error type and those paths break.
  3. Dialectdialect.name drives portable DDL. sqlite, postgresql, and mysql have live-tested paths; any other name falls back to the SQLite/PostgreSQL-flavoured DDL (right for PostgreSQL-compatible engines, wrong for e.g. MSSQL).
  4. 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=2 by 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_session power a "your devices" screen.
  • Cookie postureHttpOnly, Secure, SameSite=Lax by default. Strict mode (the default) refuses to start with cookie_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyxle_auth-0.2.0.tar.gz (61.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyxle_auth-0.2.0-py3-none-any.whl (45.5 kB view details)

Uploaded Python 3

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

Hashes for pyxle_auth-0.2.0.tar.gz
Algorithm Hash digest
SHA256 371004031089c64eb9e01bfd75e24d7bcb86a4606a66befcc3257ea6625a0dc6
MD5 0d29aa5bff65f1f902575ca4f783c69f
BLAKE2b-256 250376bfecb7901e8104e8c4df0d805795b155952a3ae406334e1b413c782490

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyxle_auth-0.2.0.tar.gz:

Publisher: publish.yml on pyxle-dev/pyxle-plugins

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pyxle_auth-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4f78b7c4b6227b8cb11a3999e33906bb7ce2036b316e86ba1f99a8968d4beaae
MD5 51d7a5a52b6142760712bc5e32b1880f
BLAKE2b-256 1d2597ad0e575d87e8536725acf2c3dd8ded6330c44395303ba73e6959468e1a

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page