CORSMiddleware
Cross-Origin Resource Sharing for API and htmx requests:
from chirp.middleware import CORSMiddleware, CORSConfig
cors = CORSMiddleware(CORSConfig(
allow_origins=["https://example.com"],
allow_methods=["GET", "POST", "DELETE"],
allow_headers=["HX-Request", "HX-Target"],
allow_credentials=True,
max_age=3600,
))
app.add_middleware(cors)
Handles preflight OPTIONSrequests automatically. Supports multiple origins, exposed headers, and credentials.
StaticFiles
Serve static assets (CSS, JS, images) from a directory:
from chirp.middleware import StaticFiles
app.add_middleware(StaticFiles(
directory="static",
prefix="/static",
))
Features:
- Path traversal protection -- uses
is_relative_to()to prevent directory escape - Index resolution -- serves
index.htmlfor directory paths - Trailing-slash redirects -- redirects
/dirto/dir/when a directory exists - Custom 404 -- optionally serve a custom 404 page
- Root prefix -- use
prefix="/"for static site hosting
# Serve a full static site from the "public" directory
app.add_middleware(StaticFiles(
directory="public",
prefix="/",
fallback="404.html",
))
SessionMiddleware
Signed cookie sessions using itsdangerous:
from chirp.middleware.sessions import SessionConfig, SessionMiddleware
app.add_middleware(SessionMiddleware(SessionConfig(
secret_key="change-me-in-production",
)))
Note
Requires theitsdangerous package. A ConfigurationError is raised at startup if it's not installed or if secret_keyis empty.
Session data is JSON-serialized into a signed cookie with sliding expiration. Access the session dict viaget_session():
from chirp.middleware.sessions import get_session
@app.route("/count")
def count():
session = get_session()
session["visits"] = session.get("visits", 0) + 1
return f"Visits: {session['visits']}"
get_session() returns a plain dict[str, Any] backed by a ContextVar-- safe under free-threading.
Configuration options:
| Option | Default | Description |
|---|---|---|
secret_key |
(required) | Signing key for the cookie |
cookie_name |
"chirp_session" |
Cookie name |
max_age |
86400(24h) |
Sliding expiration in seconds |
httponly |
True |
Prevent JavaScript access |
samesite |
"lax" |
SameSite policy |
secure |
False |
HTTPS-only (setTruein production) |
AuthMiddleware
Dual-mode authentication: session cookies (browsers) and bearer tokens (API clients). The authenticated user is stored in aContextVar, accessible via get_user()from any handler.
Setup
AuthMiddleware requires SessionMiddlewarefor session-based auth. Register sessions first:
from chirp.middleware.sessions import SessionConfig, SessionMiddleware
from chirp.middleware.auth import AuthConfig, AuthMiddleware
app.add_middleware(SessionMiddleware(SessionConfig(secret_key="...")))
app.add_middleware(AuthMiddleware(AuthConfig(
load_user=my_load_user, # async (id: str) -> User | None
verify_token=my_verify_token, # async (token: str) -> User | None
)))
You must provide at least one of load_user (session auth) or verify_token (token auth). A ConfigurationErroris raised if neither is set.
User Protocol
Your user model just needsid and is_authenticated:
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class User:
id: str
name: str
is_authenticated: bool = True
For @requires(), add a permissionsattribute:
@dataclass(frozen=True, slots=True)
class User:
id: str
name: str
is_authenticated: bool = True
permissions: frozenset[str] = frozenset()
Login and Logout
Use thelogin() and logout()helpers in your handlers:
from chirp import login, logout, Redirect
@app.route("/login", methods=["POST"])
async def do_login(request: Request):
form = await request.form()
user = await verify_credentials(form["username"], form["password"])
if user:
login(user)
return Redirect("/dashboard")
return Template("login.html", error="Invalid credentials")
@app.route("/logout", methods=["POST"])
def do_logout():
logout()
return Redirect("/")
Note
Bothlogin() and logout()regenerate the session automatically to prevent session fixation attacks. All previous session data is discarded -- the new session starts empty (with only the user ID set on login).
Route Protection
Use@login_required and @requires()to protect routes. Both work with sync and async handlers:
from chirp import login_required, requires, get_user
@app.route("/dashboard")
@login_required
def dashboard():
user = get_user()
return Template("dashboard.html", user=user)
@app.route("/admin")
@requires("admin")
def admin_panel():
return Template("admin.html")
Content-negotiated responses: browser requests redirect to login_url, API requests get 401/403 JSON errors.
Safe Redirects
When@login_required redirects to login_url, it appends a URL-encoded ?next= parameter. To safely honour this after login, use is_safe_url():
from chirp import is_safe_url, Redirect
@app.route("/login", methods=["POST"])
async def do_login(request: Request):
# ... verify credentials ...
login(user)
next_url = request.query.get("next", "/dashboard")
if not is_safe_url(next_url):
next_url = "/dashboard"
return Redirect(next_url)
is_safe_url(url) returns True only for relative paths on the same origin (starts with /, not //, no scheme). This prevents open redirect attacks where an attacker crafts a login link like /login?next=//evil.com.
Templates
AuthMiddleware auto-registers current_user()as a template global:
{% if current_user().is_authenticated %}
<a href="/profile">{{ current_user().name }}</a>
{% else %}
<a href="/login">Sign in</a>
{% endif %}
Password Hashing
Hash and verify passwords with argon2id (preferred) or scrypt (stdlib fallback):
from chirp.security.passwords import hash_password, verify_password
hashed = hash_password("user-password") # Store this
ok = verify_password("user-password", hashed) # Check on login
Note
For argon2id, install theauth extra: pip install bengal-chirp[auth]. Without it, scrypt (always available) is used as a fallback.
Configuration
| Option | Default | Description |
|---|---|---|
load_user |
None |
async (id: str) -> User | Nonefor session auth |
verify_token |
None |
async (token: str) -> User | Nonefor token auth |
login_url |
"/login" |
Redirect URL for unauthenticated browsers |
session_key |
"user_id" |
Session dict key for the user ID |
token_header |
"Authorization" |
Header for bearer tokens |
token_scheme |
"Bearer" |
Expected scheme prefix |
exclude_paths |
frozenset() |
Paths that skip auth entirely |
Tip
See the authexample for a complete working app with login, logout, and protected routes.
CSRFMiddleware
CSRF protection for form submissions:
from chirp.middleware import CSRFMiddleware
app.add_middleware(CSRFMiddleware())
Validates a CSRF token on POST, PUT, PATCH, and DELETErequests. The token is available in templates:
<form method="post" action="/submit">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<!-- form fields -->
<button type="submit">Submit</button>
</form>
Note
RequiresAppConfig(secret_key="...")to be set.
HTMLInject
Inject a snippet into every HTML response before</body>:
from chirp.middleware import HTMLInject
# Inject live-reload script in development
app.add_middleware(HTMLInject(
'<script src="/_dev/reload.js"></script>'
))
Useful for development tools, analytics scripts, or debug toolbars.
Next Steps
- Custom Middleware -- Write your own
- Overview -- How the middleware pipeline works
- Configuration -- AppConfig and secret_key