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.
112 lines
3.6 KiB
Python
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,
|
|
)
|