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:
Johan Lundberg 2026-06-05 14:12:54 +02:00
parent c175633980
commit cf2754f302
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
3 changed files with 40 additions and 5 deletions

View file

@ -1,7 +1,10 @@
from unittest.mock import MagicMock
import pytest
from httpx import AsyncClient
from porchlight.app import create_app
from porchlight.config import Settings
from porchlight.dependencies import (
get_credential_repo,
get_magic_link_repo,
@ -56,3 +59,19 @@ async def test_dependency_functions() -> None:
assert get_user_repo(request) == "user_repo_sentinel"
assert get_credential_repo(request) == "credential_repo_sentinel"
assert get_magic_link_repo(request) == "magic_link_repo_sentinel"
def test_create_app_requires_session_secret_in_production() -> None:
settings = Settings(issuer="https://op.example.com", sqlite_path=":memory:", debug=False)
with pytest.raises(RuntimeError, match="SESSION_SECRET"):
create_app(settings)
def test_create_app_allows_missing_secret_on_localhost() -> None:
settings = Settings(issuer="http://localhost:8000", sqlite_path=":memory:")
assert create_app(settings) is not 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