fix(security): require a configured session secret in production
session_secret defaulted to a random per-process value, which silently invalidates all sessions on restart and rotates the management client secret. Add _resolve_session_secret(): use the configured secret; allow a generated one only in debug or for a localhost issuer; otherwise fail startup. The management client secret is now tied to the resolved session secret. Refs: porchlight-wvx Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c175633980
commit
cf2754f302
3 changed files with 40 additions and 5 deletions
|
|
@ -82,8 +82,8 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|||
}
|
||||
oidc_server.keyjar.add_symmetric(client_id, client_cfg.client_secret)
|
||||
|
||||
# Register management client
|
||||
manage_secret = settings.session_secret or secrets.token_hex(32)
|
||||
# Register management client (stable secret tied to the session secret)
|
||||
manage_secret = app.state.session_secret
|
||||
oidc_server.context.cdb[settings.manage_client_id] = {
|
||||
"client_id": settings.manage_client_id,
|
||||
"client_secret": manage_secret,
|
||||
|
|
@ -101,6 +101,21 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|||
raise NotImplementedError("MongoDB backend not yet implemented")
|
||||
|
||||
|
||||
def _resolve_session_secret(settings: Settings) -> str:
|
||||
"""Return the session signing secret, requiring one in production.
|
||||
|
||||
A random per-process secret silently invalidates sessions on restart and
|
||||
rotates the management client secret, so it is only acceptable for local
|
||||
development (debug or a localhost issuer).
|
||||
"""
|
||||
if settings.session_secret:
|
||||
return settings.session_secret
|
||||
host = urlparse(settings.issuer).hostname or ""
|
||||
if settings.debug or host in ("localhost", "127.0.0.1", "::1"):
|
||||
return secrets.token_hex(32)
|
||||
raise RuntimeError("OIDC_OP_SESSION_SECRET must be set in production (non-debug, non-localhost issuer).")
|
||||
|
||||
|
||||
def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
if settings is None:
|
||||
settings = Settings()
|
||||
|
|
@ -116,7 +131,8 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|||
app.state.settings = settings
|
||||
|
||||
# Session middleware
|
||||
session_secret = settings.session_secret or secrets.token_hex(32)
|
||||
session_secret = _resolve_session_secret(settings)
|
||||
app.state.session_secret = session_secret
|
||||
app.add_middleware(
|
||||
CSRFMiddleware, # ty: ignore[invalid-argument-type]
|
||||
exempt_paths={"/token", "/userinfo"},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue