QUICKSTART.md 9.1 KB

Quick start

Get a development instance running on https://localhost:8080, bootstrap the first administrator, and mint a token through the authorization-code flow with PKCE. The whole path takes one terminal, one Postgres, one Redis, and one TLS certificate pair.

Purpose

For a first-time operator or integrator: the shortest correct route from a checkout to a working authorization server, and the one walkthrough that proves the grant machinery end to end. It is deliberately a tutorial, not a reference: every key, command, and endpoint is documented in the guides linked from the documentation index.

Prerequisites

  • Rust stable, as pinned by rust-toolchain.toml.
  • PostgreSQL 16 or newer with the citext extension available (the shipped compose.yaml pins postgres:18-alpine).
  • Redis 8 or newer, with persistence enabled and eviction off. The server asserts both at startup and refuses to start otherwise unless redis.noeviction_required or redis.aof_required is set false.
  • A TLS certificate and key for the public listener. If you have none yet, build a throwaway pair with the private-CA steps in DEPLOYMENT.md.
  • Docker, only if you want the container image or the local multi-replica compose.yaml stack. Contributing (the test suite, the shared test services, cargo-nextest) is covered in CONTRIBUTING.md.

Build

cargo build --locked
cargo build --locked --release

The build is offline-safe for SQL: the query! macros compile against the committed .sqlx/ cache. Regenerate that cache when a query or a migration changes; CONTRIBUTING.md has the commands.

Development configuration

The server reads configuration from D_OAUTH_* environment variables plus an optional file named by D_OAUTH_CONFIG. Export a development configuration (values here are placeholders; CONFIGURATION.md lists every key, its default, and its validation rule).

export D_OAUTH_ISSUER=https://localhost:8080
export D_OAUTH_ENVIRONMENT=development
export D_OAUTH_BIND_ADDR=0.0.0.0:8080
export D_OAUTH_HEALTH_ADDR=0.0.0.0:8081
export D_OAUTH_TLS__CERT_PATH=/path/to/tls.crt
export D_OAUTH_TLS__KEY_PATH=/path/to/tls.key
export D_OAUTH_DATABASE__URL='postgres://oauth:password@127.0.0.1:5432/oauth?sslmode=disable'
export D_OAUTH_REDIS__URL=redis://127.0.0.1:6379
export D_OAUTH_REDIS__DURABILITY=waitaof
export D_OAUTH_TOKEN_PEPPER='replace-with-32-bytes-of-random-00'
export D_OAUTH_TOTP_ENCRYPTION_KEY='replace-with-32-bytes-of-random-00'
export D_OAUTH_PII_ENCRYPTION_KEY='replace-with-32-bytes-of-random-00'

The three master keys must each be at least 32 bytes, and each is a live secret: losing the pepper invalidates every opaque token, and losing the PII key makes the sealed directory unreadable. Use the output of openssl rand -base64 32 for each placeholder; in a cluster, mount them as files through the *_FILE variants instead of the environment (DEPLOYMENT.md).

Development relaxes the database TLS requirement. Production requires sslmode=verify-full (unless database.require_verify_full is set false) and an issuer on https (no trailing slash, non-empty authority, no whitespace).

The ports above are the local-dev defaults (Postgres 5432, Redis 6379). The shared test services use 5433/6380 and are documented in CONTRIBUTING.md.

Apply the schema and bootstrap the first admin

Validate the configuration, then apply the schema once:

cargo run --bin d-oauth-server -- config check
cargo run --bin d-oauth-server -- migrate

config check prints config ok: issuer=... and exits 0 without touching Postgres or Redis; it prints the first validation error and exits 1 otherwise. serve never migrates, so migrate is a separate, once-per-deployment step.

Bootstrap the first administrator. It is created in a forced-change state and must change its password and enroll TOTP before it gains authority:

cargo run --bin d-oauth-server -- admin bootstrap --email admin@example.com

The full replacement procedure (change the password, enroll TOTP, capture recovery codes, create a named admin, disable the bootstrap account) is in RUNBOOK.md.

Apply the scope catalog, redirect URIs, resource servers, and entitlements from a JSON file, idempotently:

cargo run --bin d-oauth-server -- config sync --file ./oauth-config.json

The file format is in OPERATIONS.md. Scopes start empty, so an authorization request naming a scope that config sync did not create is rejected.

Run and probe

cargo run --bin d-oauth-server -- serve

The process serves TLS on bind_addr and plaintext health on health_addr. Check it is alive, ready, and advertising metadata:

curl -s http://127.0.0.1:8081/health/live
curl -s http://127.0.0.1:8081/health/ready
curl -sk https://localhost:8080/.well-known/oauth-authorization-server | jq .

Readiness is fail-closed: it reports ready only when Postgres, Redis, and the schema version all agree with the binary. Any other outcome is 503 {"status":"not_ready"}.

Authorization-code and PKCE walkthrough

The flow is: start at /authorize, log in and consent, receive a code on the redirect, then redeem it at /token with the PKCE verifier.

  1. Generate a verifier and its S256 challenge:

    VERIFIER=$(openssl rand -base64 64 | tr -d '=+/' | cut -c1-64)
    CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
    
  2. Visit the authorization endpoint in a browser. A confidential client must use PAR or a signed request object; a public client may use the query string:

    https://localhost:8080/authorize?response_type=code&client_id=example-client&redirect_uri=https://app.example.com/callback&scope=orders:read&state=xyz&code_challenge=$CHALLENGE&code_challenge_method=S256
    
  3. Log in, approve consent, and read the code from the redirect.

  4. Redeem the code at the token endpoint:

    curl -sk https://localhost:8080/token \
    -d grant_type=authorization_code \
    -d code="$CODE" \
    -d redirect_uri=https://app.example.com/callback \
    -d client_id=example-client \
    -d code_verifier="$VERIFIER"
    
  5. A DPoP-bound public client (dpop_bound_access_tokens: true) must present a DPoP proof with the token request; an unbound public client (the default) mints Bearer tokens with no proof. A confidential client authenticates with its secret or an mTLS certificate.

The client in step 2 must exist. Create one through POST /register (RFC 7591; see OAUTH.md) or use an operator-registered client from OPERATIONS.md. Both the POST /token dispatcher and the /register* DCR routes are wired, so step 4 redeems a code end to end.

Edge cases

  • serve never applies migrations. A replica whose schema version is behind the binary reports not ready and is pulled from service; a second migrate is a no-op.
  • Unbound public clients become bearer-capable on upgrade. Operators wanting DPoP-mandatory public clients register them with dpop_bound_access_tokens: true.
  • The health listener is plaintext and separate from the TLS listener. It is probed inside the pod network only; never publish it.
  • config sync is an upsert, not a delete: re-running the same file reports config sync: no changes, and an unchanged row does not bump updated_at (OPERATIONS.md).
  • A missing D_OAUTH_CONFIG file is not an error; the file is optional.
  • A generated bootstrap password prints only to a TTY. On a non-TTY stdout the command refuses rather than risking the secret in job logs; use --password-file.

Operational runbook

  • Validate an environment without starting the server: d-oauth-server config check.
  • Liveness: GET http://127.0.0.1:8081/health/live returns 200 {"status":"live"} without touching a dependency.
  • Readiness: GET http://127.0.0.1:8081/health/ready returns 200 {"status":"ready"} only with Postgres and Redis answering; otherwise 503.
  • The container HEALTHCHECK runs d-oauth-server healthcheck, which speaks raw HTTP/1.1 to the health listener, so an image with no shell works (DEPLOYMENT.md).
  • Day-two administration (users, scopes, redirect URIs, key rotation, backup) is OPERATIONS.md.

File and directory references

  • src/main.rs for the thin binary entry point; src/cli.rs for the command surface used above.
  • src/serve.rs for listener bootstrap, src/state/ for readiness.
  • compose.yaml for the local multi-replica stack (one Postgres, one Redis, three replicas); compose.test.yaml for the shared test services.
  • .sqlx/ for the offline query cache the locked build uses.
  • CONFIGURATION.md for every key in the block above.
  • RUNBOOK.md for admin bootstrap replacement and breach response.