Security
OwlAuth handles authentication state, provider credentials, sessions, and signing operations. Its target architecture is fail-closed and Project-scoped.
Pre-alpha
The current implementation includes the federated Project Auth, persistence, token/session, Control, signer, secret-store, Hosted UI, and SDK safeguards described for its delivered scope, with real PostgreSQL/provider/browser validation. It is still pre-alpha rather than production-supported: operators must independently review deployment TLS/proxy, secret management, database roles, backup/restore, egress, observability, upgrades, and deferred identity methods before relying on it.
Report suspected vulnerabilities through GitHub private vulnerability reporting, not a public issue. Never include real credentials, tokens, provider callback values, or personal data in a report.
Security boundaries
Project boundary
A Project is the identity and token isolation boundary. Every Project-owned row, lookup, lock, uniqueness check, idempotency record, and transaction is qualified by authoritative project_id. Globally unique IDs are defense-in-depth, not a substitute.
Applications in one Project share users and Project token trust by design. Applications that need isolation use separate Projects. belongs_to metadata does not authorize or isolate anything inside OwlAuth.
Runtime boundary
Every Runtime request is hostile until bounded, parsed, and resolved to an active Project/Application. Runtime accepts public IDs and publishable keys only as identifiers and abuse-attribution inputs; they never prove user or administrative authority.
Control boundary
Control has a distinct listener and accepts only the single API key loaded from OWLAUTH_CONTROL_API_KEY. A valid Bearer key represents the deployment operator and has full Control authority; every command still resolves the explicit Project and revalidates current target revisions. The key is process configuration, not PostgreSQL state, and Control adapters cannot mutate tables directly.
Public IDs, Project access/refresh tokens, upstream provider credentials, network location, client-certificate identity, and forwarding headers are not Control credentials. Runtime never accepts the operator key.
The optional self-hosted MCP endpoint is remote Streamable HTTP on Control and reauthenticates the operator Bearer key on every request. OwlAuth SaaS exposes a separate remote MCP endpoint that accepts only a SaaS API key and rechecks current tenant authority. A protected MCP host supplies the header; neither key enters prompts, model-visible context, tools/results, transport session IDs, or a local plugin/CLI process. Protocol tool discovery is not authorization.
Provider and redirect boundary
Provider callbacks and Application redirects are different URL classes. Both are exact registered values. Wildcards, prefix matching, user-info confusion, redirect chaining, and caller-selected callback identities are forbidden.
External Runtime and callback origins derive from trusted configuration, never arbitrary Host or forwarding headers. Proxy headers are honored only from configured trusted proxies.
Login and handoff invariants
The target flow binds Project, Application, provider registration, exact callback, exact Application redirect, Hosted Authentication UI interaction, policy revisions, and PKCE challenge in a short-lived PostgreSQL transaction.
- Application handoff requires PKCE
S256; omitted orplainchallenges fail. - Upstream provider state is high entropy, one-use, and bound to the exact Project/provider transaction.
- Provider code exchange is claimed atomically and is not blindly retried after an ambiguous result.
- Provider issuer/signature/claims and stable subject are validated by a provider-specific adapter.
- Local identity lookup uses Project + provider issuer + provider subject—not email or profile fields.
- Matching email never silently links two users.
- Provider access tokens are transient. A renewable credential may be retained only as Project/identity/generation-bound encrypted material for adapter-declared bounded profile synchronization; it is never returned to Applications, accepted for caller-selected scopes, or retained in public profile data/webhooks.
- The final Application redirect carries only a short-lived, one-use, PKCE-bound handoff ticket.
- Reusing a valid same-Project browser session requires explicit CSRF-bound confirmation, current session/user/auth-age/policy checks, and a transaction-revision race against provider/email selection; page input cannot name the user/session.
- Handoff consumption and Application-session creation commit atomically. A losing exchange receives no token material.
Email identity and Application synchronization
Passwordless email uses the same exact Application redirect and PKCE-bound handoff. After server-validated method selection, email challenge requests are enumeration-safe; canonical lookup uses a keyed digest; OTP and magic-link proofs are newest-generation, short-lived, attempt-bounded, and one-use. Challenge plus encrypted mail outbox pinned to one SMTP configuration generation and eligibility revision commit together. Proof completion revalidates that PostgreSQL status/revision, so a committed disable/compromise denies later proof even after physical delivery; SMTP delivery itself never proves identity. A matching provider email never links accounts without recent explicit proof of both identities.
Each Application receives only its policy-approved revisioned projection after its first successful handoff creates an Application-user binding. Webhook events commit durably with the projection mutation, are HMAC-signed, at-least-once, and may duplicate or arrive out of order. Receivers deduplicate immutable event IDs and compare the Application binding's projection_revision; user_revision separately identifies the Project-user base revision. Endpoint egress is exact/HTTPS, denies redirects and unsafe/private destinations by default, and webhook payloads never contain provider credentials, source payloads, SMTP data, or unrelated Project users. OwlAuth exposes no v1 Runtime directory, SCIM feed, or bulk export.
Sessions, refresh, and revocation
A Project browser session is opaque, hardened, and Project/user/browser bound. It may support sign-in reuse among Applications in that Project. Application sessions and refresh families remain Application-bound.
Refresh tokens are high-entropy opaque values stored as digests. Every generation is one-use. At most one concurrent presentation creates a successor; later or concurrent reuse revokes the whole family. Core SDKs never blindly replay an ambiguous refresh; the Application or an external stateful integration serializes refresh per family, atomically replaces the credential pair, and treats an ambiguous lost response as reauthentication.
Project, Application, user, browser session, policy, and signing revisions are revalidated before handoff or refresh commits. Project/user disablement invalidates all affected state; Application disablement affects only that Application. Already issued self-contained access tokens remain valid until short expiry unless a separately designed online check is used.
Token verification
An Application backend must verify:
- an allowlisted signing algorithm and valid signature;
kidagainst the exact Project JWKS;- exact Project
issandaud; - Project access-token
typ; iat,nbf, andexpwith bounded skew;- Application/session context required by backend policy.
Never use unverified claims to select a permissive issuer, audience, algorithm, or key endpoint. An OwlAuth Project token is not an upstream OAuth access token and should not be sent to the provider.
Durable authority and cache safety
PostgreSQL is authoritative for identity, one-use state, sessions, revocation, Project keys, and audit. The operator API key remains only in Control process configuration. Security mutations and required audit records commit in one transaction.
Redis may coordinate limits and cache public derived data. A cache hit cannot turn an authoritative denial into an allow. Redis never proves identity, consumes a ticket, rotates refresh, revokes a credential, activates a key, or establishes Project ownership.
SQLx 0.9 embeds ordered migrations, uses its PostgreSQL history/checksum validation and startup locking, and applies them before readiness in default auto mode through a capability absent from normal serving pools. DDL-free verify mode checks exact compatibility. OwlAuth adds no second checksum subsystem, and SeaORM schema sync is disabled. Migration or schema incompatibility leaves business listeners unready.
Backup and recovery
Treat PostgreSQL, the signer store, the configuration-secret store, deployment identity and URLs, the operator credential, wrapping keys, and every current or retained protection ring as one recovery set. Use PostgreSQL physical backup plus WAL archiving, or the equivalent managed-service point-in-time recovery facility, and continuously test restoration against a matched copy of external key and secret material. A database-only backup is insufficient; Redis is non-authoritative and is not restored as identity state.
Keep all traffic blocked while restoring. Restore external stores first, then PostgreSQL to the selected point, then the exact process configuration. Start isolated processes with OWLAUTH_MIGRATION_MODE=verify, require /ready, and treat a missing referenced signer, external secret, or long-term key as a recovery failure rather than generating a replacement. Start the remaining split-plane processes in verify mode, confirm durable outbox and lease recovery, and only then reopen traffic. Run any schema upgrade later as a separate reviewed operation. Backup scheduling and restore orchestration remain deployment responsibilities; the detailed checklist is in the owlauth-server operator README.
Keys and secrets
Private signing and data-protection material remains behind Project-aware provider interfaces. PostgreSQL stores public JWKs and opaque key/secret references, not ordinary private keys or provider secret bytes. Redis stores no secret/key authority.
A target key is published in Project JWKS before activation. Runtime publication leases in PostgreSQL prove that ready instances loaded the revision; Redis invalidation is not proof. Rotation keeps old public material through token and cache retention. Emergency revocation stops signing immediately after authoritative observation, while offline verifiers remain bounded by their JWKS cache behavior.
Secrets enter through protected environment/file descriptors, files, or secret managers—not ordinary CLI arguments, public configuration, health responses, panic messages, OpenAPI examples, or agent context.
Browser and request safety
Runtime serves the Hosted Authentication UI; Control serves the Management Console. They may use distinct origins or trusted disjoint non-root base paths on one origin while retaining separate internal listeners and credentials. In the shared-origin form, Runtime cookies are path-contained so browsers do not send them to Control. Shared origin deliberately shares one browser/XSS trust boundary; distinct origins provide stronger isolation.
- Cookies use
Secure,HttpOnly, host-only/narrow scope where possible, and reviewedSameSitebehavior. - Browser state changes use CSRF protection tied to the interaction/session.
- Both surfaces use restrictive CSP, framing, referrer, and cache policy, no third-party executable assets, and no service workers. Their React/Vite output is built and embedded separately per plane; Rust emits only external same-origin scripts/styles from validated manifests, and neither a generic SPA fallback nor one plane's asset tree can serve the other.
- Hosted authentication returns only to the exact stored Application redirect with a short-lived one-use PKCE-bound handoff; interaction handles and tickets are removed from browser history and redacted from referrers/logs before third-party navigation.
- The Management Console keeps the operator key only in active page memory, sends it only as a Bearer header under the configured Control base URL, and clears it on reload, close, lock, or authentication failure.
- CORS is deny-by-default and exact Application-origin based; redirect navigation is not CORS authorization.
- Bodies, headers, URIs, parameter counts, arrays, strings, decompression, concurrency, and deadlines are bounded.
- Duplicate singleton parameters, ambiguous encoding, unsupported media, and conflicting credentials fail consistently.
Observability and data disclosure
Logs, traces, metrics, errors, audit events, generated examples, and agent context must never contain provider codes/tokens or renewable credentials, email addresses/OTP/magic tokens, SMTP credentials/message bodies, webhook secrets/bodies, handoff tickets, access/refresh tokens, PKCE verifiers, cookies, provider secrets, the operator API key, private keys, full callback URLs, or complete profiles.
Redaction happens before serialization/export. Metrics use bounded-cardinality labels; belongs_to, provider subjects, arbitrary URLs, and user profiles are not labels. External errors carry stable safe codes and correlation IDs without revealing cross-Project existence or vendor internals.
Operational posture
Runtime and Control use TLS directly or through declared trusted proxies, separate internal listeners, routers, budgets, PostgreSQL pools/quotas, readiness, and rate policy. Control should bind privately; network placement supplements rather than replaces authentication.
No business listener becomes ready before typed configuration, PostgreSQL/schema compatibility, and plane-critical key/data-protection capabilities are valid. Redis failure follows endpoint-specific bounded fallback or fail-closed behavior and never weakens an invariant.
For the complete target rules, see the Project Auth flow specification, operational security specification, identity connection/email/Application sync specification, and repository SECURITY.md.