OAuth 2.1 server implementation in Rust
|
|
3 týždňov pred | |
|---|---|---|
| .cargo | 3 týždňov pred | |
| .clusterfuzzlite | 3 týždňov pred | |
| .config | 3 týždňov pred | |
| .github | 3 týždňov pred | |
| .sqlx | 3 týždňov pred | |
| .well-known | 3 týždňov pred | |
| deploy | 3 týždňov pred | |
| docs | 3 týždňov pred | |
| fuzz | 3 týždňov pred | |
| migrations | 4 týždňov pred | |
| scripts | 3 týždňov pred | |
| src | 3 týždňov pred | |
| templates | 1 mesiac pred | |
| tests | 3 týždňov pred | |
| .codegraph | 1 mesiac pred | |
| .dockerignore | 3 týždňov pred | |
| .gitignore | 3 týždňov pred | |
| AGENTS.md | 3 týždňov pred | |
| Cargo.lock | 1 mesiac pred | |
| Cargo.toml | 4 týždňov pred | |
| Dockerfile | 3 týždňov pred | |
| LICENSE.md | 1 mesiac pred | |
| README.md | 3 týždňov pred | |
| RUNBOOK.md | 3 týždňov pred | |
| SECURITY.md | 3 týždňov pred | |
| compose.test.yaml | 3 týždňov pred | |
| compose.yaml | 3 týždňov pred | |
| deny.toml | 1 mesiac pred | |
| docker-bake.hcl | 3 týždňov pred | |
| rust-toolchain.toml | 1 mesiac pred | |
| rustfmt.toml | 1 mesiac pred |
An OAuth 2.1-only authorization server written in Rust. It is deliberately not backwards compatible with OAuth 2.0: PKCE is mandatory, the implicit and resource-owner-password grants do not exist, bearer-in-query is rejected, and redirect URIs match exactly.
It implements the authorization-code, refresh-token, client-credentials, device-code, and token-exchange grants, with DPoP, mTLS, PAR, JAR, dynamic client registration, introspection, and revocation. Tokens are opaque and hashed at rest. All durable domain state is in PostgreSQL and all ephemeral state is in Redis, so replicas are interchangeable.
rust-toolchain.toml.citext extension available (the shipped
compose.yaml pins postgres:18-alpine).compose.test.yaml, and for the tests that still use testcontainers (the
unit-test helpers under src/ and the lifecycle suite).cargo-nextest for the test suite:
cargo install cargo-nextest --locked.cargo build --locked
cargo build --locked --release
The server reads configuration from D_OAUTH_* environment variables. Export a
development configuration (values here are placeholders; see
docs/CONFIGURATION.md for every key). The ports below
are the local-dev defaults (Postgres 5432, Redis 6379); the shared test
services use 5433/6380 (see Tests).
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'
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).
Validate the configuration, then apply the schema once:
cargo run --bin d-oauth-server -- config check
cargo run --bin d-oauth-server -- migrate
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
Apply the scope catalog, redirect URIs, and entitlements from a JSON file idempotently. The file format is in docs/OPERATIONS.md:
cargo run --bin d-oauth-server -- config sync --file ./oauth-config.json
Run the server. It serves TLS on bind_addr and plaintext health on
health_addr; it never migrates:
cargo run --bin d-oauth-server -- serve
Check it is alive and ready:
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 .
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.
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 '=')
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
Log in, approve consent, and read the code from the redirect.
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"
A public client must present a DPoP proof with the token request; a confidential client authenticates with its secret or an mTLS certificate.
The POST /token dispatcher and the /register* DCR routes (RFC 7591/RFC
7592) are wired, so step 4 redeems a code end to end; a client can also be
created through POST /register. See docs/OAUTH.md for the
full grant behavior.
The authoritative suite. The integration and end-to-end tests connect to the
shared test Postgres and Redis from compose.test.yaml (test defaults:
Postgres on 127.0.0.1:5433, Redis on 127.0.0.1:6380 — not the dev
5432/6379), so start the services first and
tear them down with down -v:
docker compose -f compose.test.yaml up -d
cargo nextest run --all-targets
cargo test --doc
docker compose -f compose.test.yaml down -v
Override the endpoints with D_OAUTH_TEST_POSTGRES_URL and
D_OAUTH_TEST_REDIS_URL. Each test gets its own freshly created database and
unique Redis key prefixes, so tests run in parallel and nothing flushes shared
state. Killing the suite mid-run no longer orphans containers: the lifecycle
tests still rm theirs on drop. Testcontainers remains only for the unit-test
helpers under src/ and for tests/resilience.rs, which stops and restarts
containers; tests/topology.rs is the other lifecycle suite and spawns
in-process redis-test Sentinel/Cluster fixtures that need a redis-server
binary. Docker must be running. To iterate on one area, filter by name:
cargo nextest run -E 'test(refresh_rotation)'
cargo nextest run -E 'test(admin_bootstrap)'
Quality gates:
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo nextest run --all-targets
cargo test --doc
cargo audit
cargo deny check
python3 scripts/check_doc_links.py
Build both architectures with the bake definition, then run the image. The image is non-root, has no shell, and expects a read-only root filesystem:
# Both architectures (linux/amd64 + linux/arm64)
docker buildx bake image
# Single architecture (no arm64 QEMU emulation needed)
docker buildx bake image --set image.platform=linux/amd64
docker run --rm -p 8080:8080 --read-only \
--env-file ./oauth.env \
-v /path/to/tls:/certs:ro \
d-oauth-server:latest
Building linux/arm64 on an amd64 host needs QEMU binfmt
(docker run --privileged --rm tonistiigi/binfmt --install arm64); see
docs/DEPLOYMENT.md.
The container HEALTHCHECK uses the healthcheck subcommand, which probes the
plaintext health listener without a shell or curl. See
docs/DEPLOYMENT.md.
For a local multi-replica proof, compose.yaml runs three app replicas
against one Postgres and one Redis. It is the local
analogue of the cluster manifests, and proves that a token issued by one replica
is introspectable via another; see
docs/DEPLOYMENT.md:
docker compose -f compose.yaml up --scale app=3
Cluster deployment uses a separate migration Job, a PodDisruptionBudget, and probes on the health listener. In-process mTLS requires L4/TCP passthrough at the load balancer, because an L7 ingress that terminates TLS strips the client certificate.
draft-ietf-oauth-v2-1-16 (published 3 September 2026;
an Internet-Draft, not an RFC):
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1. There are no
open gaps against that draft; see docs/OAUTH.md.WeakKeyMaterial).user disable purges the account's Redis sessions, tokens, grants, codes,
device records, and lockout counters before flipping users.disabled; that
user's live tokens then introspect inactive and refuse refresh
(invalid_grant).issuer on https and, unless
database.require_verify_full is set false, a database URL with
sslmode=verify-full.noeviction and persistence. The server asserts this at
startup and refuses to start otherwise, unless redis.noeviction_required or
redis.aof_required is set false (both default true).503 rather
than failing open.scope create|list|enable|disable|delete. Scopes are
runtime-managed and start empty.redirect-uri create|list|enable|disable|delete and
client attach-redirect-uri|detach-redirect-uri|redirect-uris.
redirect-uri create|enable|disable|delete and
client attach-redirect-uri|detach-redirect-uri accept
--kind authorize|post_logout (default authorize); post_logout URIs are
the return targets for POST /logout (GET /logout is read-only).user create|enroll-totp|disable|unlock|grant-scope|revoke-scope|scopes.admin bootstrap (see RUNBOOK.md).config sync --file and config check.pii backfill, pii verify, and pii rotate,
plus totp rewrap for the TOTP key. See docs/OPERATIONS.md.initial-access-token issue --uses <n> --ttl <seconds> [--label <text>] [--show-secret].keygen and healthcheck.Full flags are in docs/OPERATIONS.md.
ISC. See LICENSE.md.