lauren-guards: batteries-included authentication & authorization guards for the lauren web framework.
Documentation: https://lauren.dev
Source Code: https://github.com/lauren-framework/lauren-guards
lauren-guards is an authentication and authorization add-on for the
lauren Python web framework. Every guard is a factory function
that returns a class satisfying lauren.GuardProtocol — drop the result directly into
@use_guards(...) and lauren's startup validator checks the wiring before the first
request.
The key features are:
- Six authentication guards: HTTP Basic, Bearer Token, API Key, JWT (HS / RS / ES + JWKS auto-rotation), OAuth 2.0 Introspection (RFC 7662), Session Cookie.
- Three authorization guards:
require_authenticated,require_roles,require_scopes. - Two cross-cutting guards: CSRF (double-submit cookie), IP allowlist (CIDR ranges +
optional trusted-proxy
X-Forwarded-For). - Password utilities:
BcryptHasher,Argon2Hasher, andgenerate_token()for cryptographically-secure random IDs. - Sessions:
InMemorySessionStore+ aSessionStoreprotocol for plugging in Redis or Postgres in production. @publicdecorator: opt individual routes out of guard protection without changing the controller or guard configuration.- Startup-validated: all factories are decorated with
@injectable(scope=SINGLETON)so misconfigurations fail atLaurenFactory.create(...), not at runtime.
Requirements
Python 3.11, 3.12, and 3.13 are supported. Requires lauren ≥ 1.0.0.
Installation
$ pip install lauren-guards
Optional extras for heavier dependencies:
$ pip install "lauren-guards[jwt]" # adds PyJWT + cryptography (jwt_bearer)
$ pip install "lauren-guards[http]" # adds httpx (oauth2_introspection, JWKS URL)
$ pip install "lauren-guards[bcrypt]" # adds bcrypt (BcryptHasher)
$ pip install "lauren-guards[argon2]" # adds argon2-cffi (Argon2Hasher)
$ pip install "lauren-guards[all]" # all of the above
Example
Create it
from pydantic import BaseModel
from lauren import LaurenFactory, controller, get, post, module, use_guards, Path, Json
from lauren_guards import AuthUser, bearer_token, require_roles, require_scopes, public
async def verify_token(token: str) -> AuthUser | None:
# Replace with a real database / cache lookup.
if token == "good-token":
return AuthUser(id="u-42", roles=("user",), scopes=("items.read", "items.write"))
return None
BearerGuard = bearer_token(verify=verify_token)
@use_guards(BearerGuard)
@controller("/items")
class ItemController:
@get("/")
@use_guards(require_scopes("items.read"))
async def list_items(self) -> dict:
return {"items": []}
@post("/")
@use_guards(require_scopes("items.write"))
async def create_item(self) -> dict:
return {"created": True}, 201
@get("/admin")
@use_guards(require_roles("admin"))
async def admin_view(self) -> dict:
return {"access": "granted"}
@get("/status")
@public # exempt from BearerGuard
async def status(self) -> dict:
return {"status": "ok"}
@module(controllers=[ItemController])
class AppModule:
pass
app = LaurenFactory.create(AppModule, docs_url="/docs")
Run it
$ uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: [lauren] startup complete: 1 module, 1 controller, 4 routes
Check it
$ curl http://127.0.0.1:8000/items/status
{"status": "ok"}
$ curl http://127.0.0.1:8000/items/
{"detail": "Unauthorized"} # 401 — missing token
$ curl http://127.0.0.1:8000/items/ -H "Authorization: Bearer good-token"
{"items": []} # 200 — authenticated
Guard catalog
| Guard | Use when… | Extras |
|---|---|---|
bearer_token |
Opaque server-issued tokens (sessions, API tokens). | — |
basic_auth |
Simple admin endpoints protected by username/password. | — |
api_key |
Service-to-service API keys via header or query param. | — |
jwt_bearer |
Self-contained JWTs (HS/RS/ES + JWKS auto-rotation). | [jwt] |
oauth2_introspection |
Opaque OAuth 2.0 tokens validated via RFC 7662. | [http] |
session_cookie |
Browser sessions backed by SessionStore. |
— |
require_authenticated |
Any handler that just needs somebody logged in. | — |
require_roles |
RBAC: gate by named roles (admin, ops, …). |
— |
require_scopes |
OAuth-style scopes (items.read, users.write). |
— |
csrf |
State-changing endpoints behind cookie auth. | — |
ip_allowlist |
Internal endpoints behind a known proxy / VPN. | — |
Examples
JWT Bearer with JWKS rotation
from lauren_guards import jwt_bearer
# Symmetric HMAC (HS256) — for services that share a secret.
HsGuard = jwt_bearer(secret="super-secret", algorithms=["HS256"])
# Asymmetric with auto-fetched JWKS (Auth0, Cognito, Keycloak, etc.).
RsGuard = jwt_bearer(
jwks_url="https://example.auth0.com/.well-known/jwks.json",
algorithms=["RS256"],
issuer="https://example.auth0.com/",
audience="https://api.example.com",
jwks_cache_seconds=300,
)
HTTP Basic with WWW-Authenticate
from lauren import LaurenFactory
from lauren_guards import basic_auth, basic_auth_challenge_handler, BcryptHasher
hasher = BcryptHasher()
async def verify(username: str, password: str) -> AuthUser | None:
user = await db.find_user(username)
if user is None or not hasher.verify(password, user.password_hash):
return None
return AuthUser(id=user.id, roles=user.roles)
BasicGuard = basic_auth(verify=verify)
app = LaurenFactory.create(
AppModule,
global_exception_handlers=[basic_auth_challenge_handler],
)
basic_auth_challenge_handler attaches WWW-Authenticate: Basic realm="..." to every
401 response so browsers show the native credential dialog.
Session cookies
from lauren_guards import InMemorySessionStore, session_cookie, sign_cookie
store = InMemorySessionStore()
SESSION_SECRET = "my-signing-secret"
SessGuard = session_cookie(store=store, secret=SESSION_SECRET)
# In a login handler — create the session and set the cookie.
async def login(username: str) -> Response:
session = await store.create(user_id=username, ttl_seconds=3600)
signed = sign_cookie(session.id, secret=SESSION_SECRET)
return Response.json({"ok": True}).with_cookie(
"lauren_session", signed,
http_only=True, secure=True, same_site="lax",
)
Swap InMemorySessionStore for a Redis-backed implementation in multi-worker production
by implementing the three-method SessionStore protocol (create, get, delete).
CSRF protection
from lauren_guards import csrf
CsrfGuard = csrf(cookie_name="csrf_token", header_name="x-csrf-token")
Double-submit-cookie pattern: the server issues a token in a cookie; the client must echo it as a header on state-changing requests. Mismatched or absent pairs are rejected.
IP allowlist
from lauren_guards import ip_allowlist
InternalGuard = ip_allowlist(
"10.0.0.0/8",
"192.168.1.0/24",
)
@public routes
Opt individual routes out of a controller-level guard without changing the guard or controller configuration:
from lauren_guards import public
@use_guards(BearerGuard)
@controller("/api")
class ApiController:
@get("/status")
@public # exempt — no token needed
async def health(self) -> dict:
return {"status": "ok"}
@get("/profile")
async def profile(self) -> dict: # requires token
return {"user": "..."}
The AuthUser record
Every authentication guard writes an AuthUser to request.state.user. Authorization
guards read it. It is a frozen dataclass:
from dataclasses import dataclass, field
from typing import Any
@dataclass(slots=True)
class AuthUser:
id: str # stable principal identifier
roles: tuple[str, ...] = () # RBAC role strings
scopes: tuple[str, ...] = () # OAuth-style scope strings
claims: dict[str, Any] = field(default_factory=dict) # full credential payload
credential_type: str = "unknown" # "bearer" | "jwt" | "basic" | …
Read the authenticated user in any handler:
from lauren import Request
@get("/me")
async def me(self, request: Request) -> dict:
user = request.state.user
return {"id": user.id, "roles": list(user.roles)}
Guard composition
Guards run in the order they appear in @use_guards(...), outermost first:
@use_guards(jwt_bearer(...), require_scopes("admin"))
@controller("/admin")
class AdminController:
@get("/users")
@use_guards(ip_allowlist("10.0.0.0/8"))
async def list_users(self) -> dict: ...
The effective chain for GET /admin/users is:
jwt_bearer— validates the JWT and populatesrequest.state.userrequire_scopes("admin")— checks the user's scopesip_allowlist("10.0.0.0/8")— checks the source IP
Returning False from any guard yields 403 Forbidden. Raising UnauthorizedError
yields 401 Unauthorized. The rule of thumb:
- Missing or malformed credential → raise
UnauthorizedError(401) - Authenticated but not permitted → return
False(403)
Development
$ uv tool install prek # one-time
$ prek install # wires up the git hook
$ nox # lint + tests (152 passing) + typecheck
License
This project is licensed under the terms of the MIT license.
Release files for lauren-guards 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lauren_guards-0.1.0.tar.gz | 71.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lauren_guards-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 111.0 kB
Release files / lauren_guards-0.1.0.tar.gz
| Download URL | lauren_guards-0.1.0.tar.gz |
|---|---|
| Size | 71.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
13afdb638a38ef4fda8e50eabda196576e4720d52a7f7f2b67e33c3a79f800eb
|
|
BLAKE2b-256 checksum How to use checksums |
1a8eba15880c80177c7ab09d7c1513ada6ab032573a88dd08cd8876df83e0b37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency logRelease files / lauren_guards-0.1.0-py3-none-any.whl
| Download URL | lauren_guards-0.1.0-py3-none-any.whl |
|---|---|
| Size | 39.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
963add1546bfc060e73a502b5d8072141aa444d7f7070d7b03016a7ad0694184
|
|
BLAKE2b-256 checksum How to use checksums |
8b57b5205f939fcb2edcbc9f4bfa1ed040a9eea0e82bd248b11b56cb386b594f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency log