Okta OIDC (Authlib) validates state, nonce, and the id_token signature (JWKS from
discovery). The app trusts the email claim and checks it against its own allowed-users
list. Roles are re-resolved per request, so removal/demotion is immediate.
Static mode is development-only: loud startup warning, hmac.compare_digest credential
check, and it refuses to bind a non-loopback interface without PPSQ_ALLOW_INSECURE_AUTH=1.
Sessions & CSRF
Cookies: HttpOnly, SameSite=Lax (required for the OIDC callback), Secure when
cookie_secure=true. Browser-session only — no persistent cookie, no idle-timeout
cookie; closing the browser ends the session.
CSRF: a per-session token from /api/config must be echoed as X-CSRF-Token on every
state-changing /api/* request. Enforced in before_request.
The post-login next target is sanitised to a local path (safe_next) — no open redirect.
Secrets
pps.password, okta.client_secret, auth.static.password, app.secret_key are
write-only: never returned by any endpoint (redacted() emits a *_set boolean). A
blank value on save means "unchanged".
config.toml is written 0600. It is plaintext on disk — see "Known limitations".
Input validation
Config writes are validated before the atomic write (URLs, ranges, folder membership,
pipeline step order, emails/roles). Folder names may not contain commas (they'd corrupt
the localguid join in the PPS POST payload).
POST /api/actions validates targetfolder against the configured folder list — a user
cannot move mail to an arbitrary folder.
The log viewer's source is an enum key resolved to a path server-side; there is no
client-supplied path (no arbitrary file read).
Transport
Put a TLS-terminating reverse proxy in front; bind the app to loopback.
verify_tls controls whether the app verifies the PPS certificate. false (self-signed)
is common for PPS but should be a CA-bundle path where possible; the client logs a loud
warning when verification is disabled.
Known limitations (accepted for internal use)
config.toml holds secrets in plaintext (0600, never readable via the app). Real secret
management (Vault, env injection) is a future step.
Roles come from the app's own list, not Okta groups.