One-call secure-by-default middleware stack helper.
secure_stackreturns the secure-by-default middleware list in the
contract-passing order —SessionMiddleware -> CSRFMiddleware->
SecurityHeadersMiddleware— so an app author can wire the whole stack in one
line without anything being force-injected:
from chirp.middleware.stack import secure_stack
for mw in secure_stack(app.config):
app.add_middleware(mw)
Passauth=AuthConfig(...) to include AuthMiddlewarein the right place
(after sessions, before CSRF) so login auth is part of the same one-line wiring:
for mw in secure_stack(app.config, auth=AuthConfig(load_user=load_user)):
app.add_middleware(mw)
Passaudit=AuditConfig(level=...) to append an opt-in AuditMiddlewareas
the outermost leg (afterSecurityHeadersMiddleware) so it observes the
final status code of every audited request. Like the auth leg it is OFF unless
you ask for it (AuditConfig defaults to level="none"):
for mw in secure_stack(app.config, audit=AuditConfig(level="metadata")):
app.add_middleware(mw)
This is deliberately explicit-over-magic: it is a pure list-returning function
(the most inspectable/testable shape), not an auto-injecting hook. The app
author still callsadd_middlewarefor each piece, and can drop or reorder
the list, swap in custom configs, or skip the helper entirely.
The generated stack passes thesecurity_stack and csrf_session
order contracts (same classes, same order) and inherits the Wave 2
SessionConfig.secure="auto" posture: the session cookie's Secureflag is
resolved at freeze fromconfig.env (Truefor staging/production), not
fromconfig.debug. Coupling cookie security to debugis the footgun the
env-aware design exists to avoid, so this helper never readsdebug.
middleware.stack
| Name | Type | Default | Description |
|---|---|---|---|
type
|
|
— | |
qualified_name
|
|
— | |
element_type
|
|
— | |
description
|
|
— | |
source_file
|
|
— | |
line_number
|
|
— | |
is_autodoc
|
|
— | |
autodoc_element
|
|
— | |
_autodoc_template
|
|
— | |
_autodoc_url_path
|
|
— | |
_autodoc_page_type
|
|
— | |
title
|
|
— | |
doc_content_hash
|
|
— |
Symbols on this page
secure_stack
function
def secure_stack(config: AppConfig, *, auth: AuthConfig | None = None, session: SessionConfig | None = None, csrf: CSRFConfig | None = None, headers: SecurityHeadersConfig | None = None, audit: AuditConfig | None = None, redis_url: str | None = None) -> list[Any]
Return the secure-by-default middleware stack, in contract-passing order.
The returned list is[SessionMiddleware, CSRFMiddleware, SecurityHeadersMiddleware] — exactly the order the security_stackand
csrf_sessioncontracts require (Session before CSRF, since the CSRF token
lives in the session). Passauth to also include AuthMiddleware,
placed after the session leg and before CSRF, giving
[SessionMiddleware, AuthMiddleware, CSRFMiddleware, SecurityHeadersMiddleware]. Nothing is force-injected: the caller adds each
middleware itself, typically::
for mw in secure_stack(app.config, auth=AuthConfig(load_user=load_user)):
app.add_middleware(mw)
Defaults are derived from config:
- The session
secret_keyis read fromconfig.secret_key. - The session cookie's
Secureflag is left asSessionConfig'ssecure="auto"default. Wave 2 freeze-resolution turns"auto"intoTrueforenv in ("staging", "production")andFalseotherwise (notably local development), keyed onconfig.env— neverconfig.debug. So a default-config app shipsSecuresession cookies in production without the author touching the field, while local dev keeps them non-Secure to avoid logging dev users out over HTTP.
Passsession/csrf/headersto override any leg with your own
fully-built config; when omitted, sane defaults are constructed. Pass
redis_url to back sessions with RedisSessionStore(requires the
redisextra) instead of the default signed-cookie store; it is ignored
when an explicitsessionconfig is supplied (that config already owns its
store choice).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
config
|
AppConfig
|
— | The app config. Supplies ``secret_key`` and (via freeze resolution) the ``env``-derived cookie ``Secure`` posture. |
auth
|
AuthConfig | None
|
None
|
Optional ``AuthConfig``. When provided, ``AuthMiddleware(auth)`` is inserted after the session leg and before CSRF (so a CSRF rejection's audit event already carries the resolved ``user_id``). When omitted, no auth leg is added — wire ``AuthMiddleware`` yourself if needed. |
session
|
SessionConfig | None
|
None
|
Optional explicit ``SessionConfig``. Overrides the derived default entirely (including ``redis_url``). |
csrf
|
CSRFConfig | None
|
None
|
Optional explicit ``CSRFConfig``. Defaults to ``CSRFConfig()``. |
headers
|
SecurityHeadersConfig | None
|
None
|
Optional explicit ``SecurityHeadersConfig``. Defaults to ``SecurityHeadersConfig()``. |
audit
|
AuditConfig | None
|
None
|
Optional ``AuditConfig``. When provided, ``AuditMiddleware(audit)`` is appended as the **outermost** leg (after ``SecurityHeadersMiddleware``) so it wraps the whole chain and observes the final status code. OFF unless requested — the default ``AuditConfig`` is ``level="none"``. When omitted, no audit leg is added. |
redis_url
|
str | None
|
None
|
Optional Redis URL. When set (and ``session`` is omitted), sessions are stored in ``RedisSessionStore`` rather than a signed cookie. |
View source · /home/runner/work/chirp/chirp/site/../src/chirp/middleware/stack.py:1