Skip to main content

guest-auth

PyPI CI Python License: MIT

Not a replacement for real authentication. A static-allowlist invite-token gate for pre-production demos and invite-only previews. Pure-ASGI middleware that plugs into any Starlette / FastAPI app in ~5 lines.

Give a tester a link like https://your-app.example.com/?token=tok_abc123; the middleware validates the token against an allowlist you own, exchanges it for an httpOnly cookie, and attaches an identity (token + human-readable recipient label) to the request via a ContextVar that reaches sync endpoints in the threadpool as well.

It also ships guest-auth-tokens, a small offline CLI that turns a list of names into tokens and shareable invite links — see below.

Extracted from a production app and now used by three: Pitchcraft and JobScout (both private) and Rulebook, which is public and builds a full RBAC layer — capability vocabulary, role bundles, require_capability — on top of this library's identity and claims.

Adopting this in a new app? See docs/integration.md for the dependency-injection pattern, the gotchas (init order, pure-ASGI vs BaseHTTPMiddleware, ContextVar propagation), claims resolution, and the constructor reference.


What this is NOT

Naming a library *-auth invites expectations it doesn't meet. To be explicit:

  • No password handling, no MFA, no OAuth / OIDC, no account lifecycle. The credential is an opaque token you generate and hand to a tester.
  • No token rotation, expiry, revocation-list, or signed cookies. The cookie is httpOnly + Secure + SameSite=Lax with a 30-day convenience lifetime; revoking access means removing the token from the allowlist and redeploying.
  • No rate limiting. Compose one separately (e.g. llm-cost-governor ships an IP rate limiter).
  • No policy engine. The identity can carry role / scopes claims (see below), but guest-auth never interprets or enforces them — no capability vocabulary, no role→permission bundles, no require_x decorator. That's authz over the claims and it lives in your app.
  • Not audited for adversarial threat models. This is a gate to keep pre-production URLs off the open web and attribute per-tester activity, not a substitute for real identity infrastructure. If you're gating production PII or payment flows, use something else.

The value the library provides — a well-scoped ASGI middleware that publishes a per-request identity ContextVar that reaches sync endpoints — is genuinely useful and hard to get right (the "pure-ASGI vs BaseHTTPMiddleware" trap is subtle). Everything above is deferred, not planned.


Install

pip install guest-auth

Requires Python 3.11+. The only runtime dependency is starlette, which any ASGI host already has.


Quick example

from dataclasses import dataclass, field
from fastapi import FastAPI
from guest_auth import InviteAuthMiddleware, get_current_guest


@dataclass
class Settings:
    demo_mode: bool = True
    invite_tokens: dict = field(
        default_factory=lambda: {"tok_abc123": "Jane Tester"}
    )


settings = Settings()
app = FastAPI()


@app.get("/")
def home():
    guest = get_current_guest()
    return {"welcome": guest.recipient if guest else "anonymous"}


app.add_middleware(
    InviteAuthMiddleware,
    config=settings,
    # Optional — pre-rendered HTML for the 401 / welcome page.
    # Omit to use the built-in "This site is currently invite-only." body.
    welcome_html="<h1>Preview build</h1><p>Ask jane@example.com for a link.</p>",
)

Now:

  • GET /?token=tok_abc123 → 302 to /, sets guest_session cookie.
  • GET / with the cookie → returns {"welcome": "Jane Tester"}.
  • GET / without a cookie → 401 with the welcome page.
  • Flip settings.demo_mode = False → gate becomes a complete pass-through with no restart.

Core concepts

GuestAuthConfig (Protocol)

The middleware takes a config object that satisfies:

class GuestAuthConfig(Protocol):
    demo_mode: bool
    invite_tokens: Mapping[str, str]  # token → recipient label

Both attributes are read at request time, so mutating a live config instance (a pydantic BaseSettings, a dataclass, whatever) takes effect on the next request without rebuilding the middleware.

GuestIdentity + get_current_guest()

On a successful cookie match, the middleware sets a request-scoped ContextVar with GuestIdentity(token=..., recipient=..., role=..., scopes=...). Anywhere downstream — sync or async, including code paths in Starlette's threadpool — get_current_guest() returns it or None.

Because the middleware is pure-ASGI (not BaseHTTPMiddleware), the ContextVar survives into the threadpool that runs def (sync) endpoints. See the integration doc for why this matters.

Claims: role and scopes

Optionally, the identity carries authorization claims. guest-auth defines their structure and never their meaning — any string is a role, any strings are scopes:

from guest_auth import GuestClaims

def resolve_claims(token: str) -> GuestClaims:
    return GuestClaims(role="level5", scopes=("rules", "faq"))  # your store

app.add_middleware(
    InviteAuthMiddleware, config=settings, claims_resolver=resolve_claims
)

The resolver may be sync or async; a sync one runs in Starlette's threadpool, so a resolver that reads a bucket doesn't stall the event loop. Caching is the resolver's job.

Two contracts to know: resolution failure is soft (log + empty claims, never a 401), and consequently role=None means unresolved, not unprivileged — map it to your own floor explicitly. Full detail in the integration doc.

PathScopedContextVarMiddleware (bonus)

An adjacent generic that ships in the same package: match a regex against scope["path"], publish an extracted value on a caller-supplied ContextVar for the duration of the request. Same pure-ASGI rationale as the auth middleware. Use for /api/things/{id}/… style path-scoped ContextVars (session IDs, tenant IDs, whatever).


Minting invite tokens

Installing the package also installs guest-auth-tokens, a small offline helper for the chore every app behind this middleware repeats: turn a list of names into tokens and links you can send people. Stdlib only — it adds no dependency.

Write one recipient per line (an inline # adds a private note that stays local):

Alice
Mike        # met at worlds
Bob Smith

Then:

guest-auth-tokens gen --names secrets/guests.txt --out secrets/tokens.json --links secrets/links.md --base-url https://your-app.example.com

That writes a {token: label} JSON — feed it to config.invite_tokens however your app prefers — and a markdown file of shareable invite links.

Re-running is merge-preserving: a name that already has a token keeps it, so links you've already sent never break. A name you remove is reported rather than revoked — its token stays valid until you delete it by hand, because revoking someone's access shouldn't be a side effect of regenerating a file.

Both output files contain working credentials. The tool checks whether they're covered by a .gitignore and warns loudly if not.

Note what this deliberately isn't: there's no live allowlist management, no storage backend, and no cloud dependency. Where your app keeps its allowlist and how it refreshes it stays yours.

Development

git clone https://github.com/ecoop/guest-auth
cd guest-auth
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check src tests

CI runs on Python 3.11 through 3.14 via GitHub Actions.

Versioning

Currently v0.2.1. Semver from v1.0.0 onward; anything before is "shipped but pre-stable API — expect breaking changes."

Contributing

Issues and pull requests welcome. For substantive changes, open an issue first — this library has a deliberately small surface and staying small is a feature.

License

MIT. See LICENSE.


Last updated: 2026-09-08

Download files

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

Source Distribution

guest_auth-0.2.1.tar.gz (31.0 kB view details)

Uploaded Source

Built Distribution

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

guest_auth-0.2.1-py3-none-any.whl (22.4 kB view details)

Uploaded Python 3

File details

Details for the file guest_auth-0.2.1.tar.gz.

File metadata

  • Download URL: guest_auth-0.2.1.tar.gz
  • Upload date:
  • Size: 31.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for guest_auth-0.2.1.tar.gz
Algorithm Hash digest
SHA256 7237f36dddbb98f3e37287441899c1c3ff3e4bf1c84d04c23e9e79ec5d1792e2
MD5 886cba9f283a5ccb7fe85ce4ac3f2b6f
BLAKE2b-256 86f21e4847340631106abee777eca88e5e1474ea505ff17f40853a404c737c1f

See more details on using hashes here.

Provenance

The following attestation bundles were made for guest_auth-0.2.1.tar.gz:

Publisher: release.yml on ecoop/guest-auth

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

File details

Details for the file guest_auth-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: guest_auth-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 22.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for guest_auth-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9184667ae7f3d7e83df9deb1ab64c74a51f2741c2647fcbfab450f9f11e5077e
MD5 477ab04d605fce5b7bab2edfe5c28842
BLAKE2b-256 585848369223c6874a276876c58bbcab100c32cd117e02619b91d9ffba07fc30

See more details on using hashes here.

Provenance

The following attestation bundles were made for guest_auth-0.2.1-py3-none-any.whl:

Publisher: release.yml on ecoop/guest-auth

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

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.1

2 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