From cba63280fb4358b2f97ebd83ea286c8121fcd4b4 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Mon, 8 Jun 2026 10:26:57 +0200 Subject: [PATCH] fix(security): set an explicit session cookie lifetime The session cookie relied on Starlette's default max_age (two weeks), which is easy to miss and longer than an OP session should live. Add a session_max_age setting (default 8 hours) and pass it to SessionMiddleware. Refs: porchlight-1lg Co-Authored-By: Claude Opus 4.8 (1M context) --- src/porchlight/app.py | 1 + src/porchlight/config.py | 1 + tests/test_app.py | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/src/porchlight/app.py b/src/porchlight/app.py index e07d51f..c186d8d 100644 --- a/src/porchlight/app.py +++ b/src/porchlight/app.py @@ -143,6 +143,7 @@ def create_app(settings: Settings | None = None) -> FastAPI: secret_key=session_secret, same_site="lax", https_only=settings.session_https_only, + max_age=settings.session_max_age, ) # Rate limiting diff --git a/src/porchlight/config.py b/src/porchlight/config.py index a8225d2..d2e6cc1 100644 --- a/src/porchlight/config.py +++ b/src/porchlight/config.py @@ -48,6 +48,7 @@ class Settings(BaseSettings): # Session session_secret: str | None = None # If None, a random secret is generated per process session_https_only: bool = True + session_max_age: int = 28800 # Cookie lifetime in seconds (default 8 hours) # WebAuthn user verification requirement: "preferred" (default), "required", # or "discouraged". Identity providers may want "required". diff --git a/tests/test_app.py b/tests/test_app.py index 14c3aca..5ce3bcf 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -75,3 +75,12 @@ def test_create_app_allows_missing_secret_on_localhost() -> None: def test_create_app_allows_missing_secret_in_debug() -> None: settings = Settings(issuer="https://op.example.com", sqlite_path=":memory:", debug=True) assert create_app(settings) is not None + + +async def test_session_cookie_has_explicit_max_age(client: AsyncClient) -> None: + # Visiting /login establishes a session (CSRF token), setting the cookie. + res = await client.get("/login") + set_cookies = res.headers.get_list("set-cookie") + session_cookies = [c for c in set_cookies if c.startswith("session=")] + assert session_cookies, "no session cookie set" + assert "Max-Age=28800" in session_cookies[0]