Built-in Middleware

CORS, StaticFiles, Sessions, Auth, CSRF, and HTMLInject

5 min read 975 words

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 -- usesis_relative_to()to prevent directory escape
  • Index resolution -- servesindex.htmlfor directory paths
  • Trailing-slash redirects -- redirects/dir to /dir/when a directory exists
  • Custom 404 -- optionally serve a custom 404 page
  • Root prefix -- useprefix="/"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