porchlight/src/porchlight/config.py
Johan Lundberg 58da15c825 Implement reverse-proxy forward-auth
Add a /forward-auth endpoint that validates the existing Porchlight
session for reverse proxies and returns identity headers for
authenticated active users. Unauthenticated or inactive sessions
redirect to login, with optional post-login return targets reconstructed
from proxy headers and constrained by an allowed host list.

Also add forward_auth_allowed_redirect_hosts configuration, preserve
validated forward-auth return targets through login/session reset,
exempt the endpoint from CSRF for proxy subrequests, document
configuration, and cover the flow with tests.
2026-07-04 23:29:26 +02:00

112 lines
3.6 KiB
Python

# src/porchlight/config.py
import os
from enum import StrEnum
from typing import Any
from pydantic import BaseModel
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, TomlConfigSettingsSource
class StorageBackend(StrEnum):
SQLITE = "sqlite"
MONGODB = "mongodb"
class ClientConfig(BaseModel):
client_secret: str
redirect_uris: list[str]
response_types: list[str] = ["code"]
scope: list[str] = ["openid"]
token_endpoint_auth_method: str = "client_secret_basic"
class Settings(BaseSettings):
model_config = {"env_prefix": "OIDC_OP_", "toml_file": "porchlight.toml"}
# Class-level bridge to pass _toml_file into the classmethod
# settings_customise_sources. Not thread-safe, but Settings is
# only instantiated once at startup.
_toml_file_override: str | None = None
# Core
issuer: str
debug: bool = False
# Storage
storage_backend: StorageBackend = StorageBackend.SQLITE
# SQLite
sqlite_path: str = "data/oidc_op.db"
# MongoDB
mongodb_uri: str = "mongodb://localhost:27017"
mongodb_database: str = "oidc_op"
# Management RP
manage_client_id: str = "manage-app"
# 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)
# Content-Security-Policy applied to all responses.
content_security_policy: str = (
"default-src 'self'; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
)
# WebAuthn user verification requirement: "preferred" (default), "required",
# or "discouraged". Identity providers may want "required".
webauthn_user_verification: str = "preferred"
# Magic links
invite_ttl: int = 86400 # seconds
# Rate limiting (disable for e2e/load tests that authenticate repeatedly)
rate_limit_enabled: bool = True
# Number of trusted reverse proxies in front of the app. When > 0, the
# client IP for rate limiting is taken from X-Forwarded-For, counting this
# many hops from the right. Keep 0 unless deployed behind a known proxy.
trusted_proxy_count: int = 0
# Reverse-proxy forward-auth. Hosts listed here are eligible post-login
# redirect targets when /forward-auth receives X-Forwarded-* request
# metadata. Supports exact hosts, host:port, and explicit "*.example.com"
# wildcard suffixes.
forward_auth_allowed_redirect_hosts: list[str] = []
# Signing keys
signing_key_path: str = "data/keys"
# Theme
theme: str = "default"
# OIDC clients
clients: dict[str, ClientConfig] = {}
def __init__(self, _toml_file: str | None = None, **kwargs: Any) -> None:
Settings._toml_file_override = _toml_file
try:
super().__init__(**kwargs)
finally:
Settings._toml_file_override = None
@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
toml_file = (
cls._toml_file_override
or os.environ.get("OIDC_OP_CONFIG_FILE")
or settings_cls.model_config.get("toml_file")
)
return (
env_settings,
TomlConfigSettingsSource(settings_cls, toml_file=toml_file),
init_settings,
)