The d-OAuth-Server authorization server is a single Rust process with two listeners: a TLS public listener for OAuth traffic and a plaintext health-only listener for probes. All durable domain state lives in PostgreSQL and all ephemeral state lives in Redis. There is no in-process state that a second replica would need, so replicas are interchangeable.
This guide explains how the code is laid out, what happens to a request from socket to response, and where the Postgres/Redis boundary sits. Use it to find the module that owns a behavior before you change it.
The crate is one library (src/lib.rs) plus a thin binary (src/main.rs). The
binary parses the CLI and maps the result to an exit code; all behavior lives in
the library.
| Module | Owns |
|---|---|
src/config.rs, src/config/ |
Layered configuration load from D_OAUTH_* env vars and an optional file, then fail-fast validation. See CONFIGURATION.md. |
src/keys.rs |
The versioned KeyRing: token peppers, the TOTP encryption key, and the PII field-encryption key, each under a key id with an optional retained previous key. |
src/crypto/ |
Constant-time comparison, PKCE, opaque token mint/hash, TOTP, and password/client-secret hashing. |
src/crypto/field.rs |
Versioned AES-256-GCM field codec and keyed-HMAC blind index for the PII columns. |
src/jose/ |
DPoP proof verification, JAR request objects, private_key_jwt client assertions, and JWK parsing. |
src/db/ |
Postgres repositories for the six domain tables. See DATABASE.md. |
src/store/ |
Redis ephemeral state and the durability barrier. See REDIS.md. |
src/protocol/ |
OAuth endpoints and grants, plus the subject-scoped Redis purge (src/protocol/purge.rs, purge_subject, used by user disable). |
src/ui/ |
Server-rendered login, consent, and device pages (Askama templates) plus the embedded same-origin stylesheet served at /assets/app.css. |
src/middleware/ |
Request id, tracing and redaction, rate limiting, security headers, and audit. |
src/resilience.rs |
Load-shed, concurrency-limit, and timeout guard between CORS and rate limiting. |
src/routes.rs |
Router assembly and the middleware order. |
src/serve.rs, src/tls.rs |
Listener bootstrap and the custom TLS accept loop. |
src/state.rs, src/state/ |
AppState, the topology-aware Redis connection, health endpoints. |
src/cli.rs, src/cli/ |
The operator command tree. See OPERATIONS.md. |
src/error.rs |
AppError: the client-safe OAuth error type and its HTTP mapping. |
Every behavior belongs to exactly one module. When a change spans modules, the new logic goes in the one that owns the behavior; the others call into it.
A public request enters through the TLS listener and passes through one
tower stack before reaching a handler. The order, from outermost to innermost,
is fixed in build_router_with_options in src/routes.rs:
X-Request-Id and opens the redacted
http_request tracing span. Tracing and redaction live here.Referrer-Policy, X-Content-Type-Options, a CSP
with frame-ancestors 'none', HSTS on the TLS listener, and
Cache-Control: no-store on every response except /.well-known/… metadata.
Sits just inside the request-id layer so short-circuiting layers below
(CORS preflights, resilience 503s, rate-limit 429/503s, body-limit 413s)
still carry the headers.(peer_ip, client_id) composite, because
client_id is read before client authentication and must never select a
global bucket on its own; the per-IP bucket is the primary control. Health
and metadata have no rule and pass through; those handlers fail closed on
their own.A handler that needs a dependency takes AppState and calls a repository or the
store. Errors are converted to AppError, which maps to an OAuth error body and
the correct HTTP status. A dependency outage on a security path maps to 503
with Retry-After; it never fails open.
bind_addr (default 0.0.0.0:8080) and speaks TLS
only. It is driven by a custom hyper-util plus tokio-rustls accept loop
in src/tls.rs, not axum::serve, because the loop must inject the verified
client-certificate chain into request extensions for mTLS.health_addr (default 0.0.0.0:8081) in
plaintext and serves exactly two routes, /health/live and /health/ready.
Every OAuth route returns 404 there by construction. It is not exposed
through the Service or an ingress; only the pod network probes it.The health address must not be loopback. Kubelet probes originate from the node
network namespace and cannot reach pod-loopback, so serve refuses a loopback
health_addr at startup.
The split is strict and drives the whole design:
oauth_clients, users,
scopes, user_scopes, redirect_uris, client_redirect_uris, plus sqlx's
_sqlx_migrations. Nothing is written to Postgres per request. A failed login
does not touch Postgres; the counter lives in Redis. Introspection and refresh
do re-read Postgres on every use: a non-empty subject resolving to a disabled
user yields inactive (introspection) or invalid_grant (refresh) without
minting, and refresh re-reads the user's current entitlements; an empty or
unknown subject stays live. user disable purges the user's Redis state
before flipping users.disabled, failing closed on a Redis error.private_key_jwt replay markers, and
rate-limit counters.The consequence is that a Redis flush loses ephemeral state but not clients, users, scopes, or redirect URIs. Registration access tokens are the one token kind stored as a hash in Postgres, so client management survives a Redis loss.
AppState.redis is the RedisConn enum (src/state/redis.rs), not a concrete
connection type. One enum lets single-node, Sentinel, and Cluster share every
call site:
redis::aio::ConnectionManager.MOVED and ASK redirects recover without
a restart.Atomic multi-key operations use Lua scripts keyed under the {state} hash tag,
so they stay on a single slot. See REDIS.md.
issuer is validated as absolute https.secrecy/zeroize types and never reach a Debug output,
a log, or an error body.503./metadata reads the enabled scope catalog from Postgres on every request, so
a CLI catalog mutation is visible immediately on every replica; when Postgres
is down it returns 503 rather than advertising an empty or unknown scope
list.GET http://<pod-ip>:8081/health/live returns 200 {"status":"live"}
without touching a dependency.GET http://<pod-ip>:8081/health/ready returns 200 {"status":"ready"}
only when Postgres and Redis answer and the schema version matches the binary.
Any other outcome is 503 {"status":"not_ready"}.SIGTERM the process flips readiness to 503 first, waits a one second
propagation grace, drains in-flight requests under a 30 second deadline, then
closes the pools and exits 0.X-Request-Id value to correlate a request
with its logs.src/lib.rs for the authoritative module list.src/routes.rs for endpoint inventory and middleware order.src/serve.rs for listener bootstrap and shutdown semantics.src/state.rs, src/state/health.rs, src/state/redis.rs,
src/state/sentinel.rs for state, health, and topology.src/tls.rs for the TLS accept loop.