How to build, test, and change the d-OAuth authorization server. The rules here are enforced by CI, so a change that skips them will not merge.
This guide is for anyone editing the Rust source. It covers prerequisites, the quality gates, module conventions, the test-driven workflow, commit style, and the documentation contract. Operators looking to run the server should start with the README and OPERATIONS.md.
rust-toolchain.toml (stable, with rustfmt
and clippy). rustup installs it automatically on first build.cargo-nextest for the test suite:
cargo install cargo-nextest --locked.compose.test.yaml (docker compose -f compose.test.yaml up -d), and for
the lifecycle tests that still use testcontainers.sqlx-cli only when you add or change a SQL query or a migration:
cargo install sqlx-cli --locked --features runtime-tokio,tls-rustls-ring.The crate uses edition 2024 and the rustfmt settings in rustfmt.toml
(2-space indent, StdExternalCrate import grouping).
cargo build --locked # debug
cargo build --locked --release # release (thin LTO, stripped)
The build is offline-safe for SQL: with SQLX_OFFLINE=true the query! macros
compile against the committed .sqlx/ cache, so no database is needed at build
time. The Docker build relies on exactly this.
Run all of these before opening a change. CI runs the same set.
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo nextest run --all-targets
cargo test --doc
cargo audit
cargo deny check
Start the shared test services before cargo nextest run --all-targets:
docker compose -f compose.test.yaml up -d, then tear them down with
docker compose -f compose.test.yaml down -v. cargo nextest run is the
authoritative test entry point. It runs unit, integration, and end-to-end tests;
the integration tests use the shared services, and the lifecycle suites keep
testcontainers. cargo test --doc runs the doc examples separately because
nextest does not cover doctests.
While iterating on one area, filter to just that area:
cargo nextest run -E 'test(refresh_rotation)'
cargo nextest run -E 'test(admin_bootstrap)'
A delegated or subagent change runs only the narrowest filter that covers the touched code. The full suite runs once, at integration time.
Integration tests connect to the shared Postgres and Redis started from
compose.test.yaml:
docker compose -f compose.test.yaml up -d
cargo nextest run --all-targets
docker compose -f compose.test.yaml down -v
The defaults are Postgres on 127.0.0.1:5433 and Redis on 127.0.0.1:6380,
overridable through D_OAUTH_TEST_POSTGRES_URL and D_OAUTH_TEST_REDIS_URL.
Each test isolates itself with a freshly created database and unique Redis key
prefixes, so parallel tests never share state and nothing is flushed.
Migrations are never run by the test process against the shared postgres
database; a test that asserts on a migration applies it to its own fresh
database.
The lifecycle tests are the exception. tests/resilience.rs stops and restarts
containers through testcontainers; the guards synchronously rm them on drop,
so killing the suite mid-run no longer orphans containers. tests/topology.rs
spawns in-process redis-test Sentinel/Cluster fixtures, which need a real
redis-server binary. Unit-test helpers under src/ also still use
testcontainers. They need a working Docker daemon and, for the in-process
topology fixtures, the redis-server binary.
The crate is one library (src/lib.rs) plus a thin binary (src/main.rs).
Every module has a single responsibility:
| Area | Path | Owns |
|---|---|---|
| Configuration | src/config.rs, src/config/ |
Layered env/file load and fail-fast validation. |
| Keys | src/keys.rs |
Versioned KeyRing for the token pepper and TOTP encryption key. |
| Crypto primitives | src/crypto/ |
Constant-time compare, PKCE, token mint/hash, TOTP, secret hashing. |
| JOSE | src/jose/ |
DPoP, JAR, private_key_jwt verification, JWK parsing. |
| Postgres repositories | src/db/ |
The six domain tables. |
| Redis store | src/store/ |
Ephemeral state: codes, tokens, grants, sessions, PAR, device, DCR, rate limits. |
| Protocol handlers | src/protocol/ |
OAuth endpoints and grants. |
| Server-rendered UI | src/ui/ |
Login, consent, and device pages. |
| Middleware | src/middleware/ |
Request id, tracing/redaction, rate limit, security headers, audit. |
| Routing and serving | src/routes.rs, src/serve.rs, src/tls.rs |
Router assembly and the two listeners. |
| Operator CLI | src/cli.rs, src/cli/ |
Every administrative command. |
Rules that apply everywhere:
#![forbid(unsafe_code)] is on the crate.unwrap, expect, or panic in request paths. They are denied by the
clippy lints in Cargo.toml.thiserror; map dependency failures to the correct
client-safe error rather than stringifying them.secrecy and zeroize. A type that holds a secret must not have
a Debug impl that prints it.subtle) for any secret or token comparison.main.rs free of business logic.Every behavior change follows red, green, refactor:
Write the test against the shared Postgres and Redis from
compose.test.yaml (defaults 127.0.0.1:5433 and 127.0.0.1:6380, override
with D_OAUTH_TEST_POSTGRES_URL / D_OAUTH_TEST_REDIS_URL), with a fresh
database and unique Redis prefixes per test. Keep testcontainers for the
stop/start lifecycle cases only. Prefer a real object or an in-memory fake over
a mock. Assert the observable contract, not an implementation dump.
One commit per logical change, in conventional-commit form:
feat(token): add token-exchange grant
fix(store): fail closed when the durability barrier is missed
docs(agents): document the module map
test(resilience): cover Redis restart recovery
The subject is imperative and lowercase after the type. The body explains why, and links the evidence file when the task process requires one. Never commit a secret, a real token, or key material.
The docs are part of the code, not an afterthought. Any behavior change updates the relevant guide in the same commit:
docs/OAUTH.md.docs/SECURITY.md.docs/ARCHITECTURE.md.docs/CONFIGURATION.md.docs/DATABASE.md.docs/REDIS.md.docs/DEPLOYMENT.md.docs/OPERATIONS.md.Then run the link checker, which CI also runs:
python3 scripts/check_doc_links.py
A command in a doc must match the real CLI. If you change a flag, change the doc in the same commit.