Nazo Auth Server is a Rust-native OAuth 2.1 and OpenID Connect authorization server for self-hosted deployments. The implementation favors explicit profile boundaries, sender-constrained token support, and repeatable conformance evidence.
The current implementation covers the authorization-server surface: authorization code with PKCE, token issuance, refresh tokens, PAR, signed request objects, DPoP, mTLS sender constraints, JWKS, discovery, UserInfo, token management, and a compact identity/admin data plane.
- Package:
nazo-oauth-server - Language: Rust 2024
- License: Apache-2.0
- Runtime dependencies: PostgreSQL and Valkey
- Main branch policy: work happens on
main - Chinese documentation: see README.zh-CN.md
- Conformance evidence: see docs/conformance
- Deployment guide: see docs/deployment.md and docs/deployment.zh-CN.md
- Ecosystem onboarding decisions: see docs/ecosystem-onboarding.md
- PostgreSQL and Valkey operations: see docs/ha-operations.md
- Resource server verifier: see docs/resource-server-verifier.md
- Current scope: see docs/roadmap.md
- Security policy: see SECURITY.md
- Release security: see docs/release-security.md
- Change history: see CHANGELOG.md
- OAuth authorization code flow with S256 PKCE.
- Refresh-token rotation, token-family reuse detection, and atomic authorization code consumption.
- Client credentials, refresh token, revocation, and introspection endpoints.
- OpenID Connect discovery, OAuth Authorization Server Metadata, JWKS, ID Token, and UserInfo.
- PAR and JAR support, including signed request objects with
EdDSA,RS256,ES256, andPS256. client_secret_basic, compatibilityclient_secret_post,private_key_jwt, public clients, and mTLS client authentication. High-security clients useprivate_key_jwtor mTLS rather thanclient_secret_post.- DPoP proof validation, nonce handling, sender-constrained access tokens, and DPoP-bound UserInfo.
- mTLS sender-constrained access tokens through a trusted reverse-proxy certificate forwarding boundary.
- Server signing key rotation with active and previous JWKS publication.
- Pairwise subject identifiers.
- Cookie sessions, CSRF protection, security response headers, and structured audit events.
- HTTPOnly session cookie flow; login responses do not expose session identifiers in JSON.
- PostgreSQL persistence with Rust-native migrations.
- Valkey-backed sessions, security state, replay prevention, PAR handles, and rate limiting.
- User, profile, avatar, OAuth client, grant, and access-request management APIs.
- RFC 8707
resourceparameter support for token requests, including repeated resource indicators mapped to JWT access-tokenaudarrays. The olderaudienceparameter remains as a single-audience project extension. - RFC 9396-style Rich Authorization Requests through
authorization_detailson authorization, PAR, and signed request object inputs. Supported detail types are advertised in OAuth metadata and bound into consent, authorization codes, refresh tokens, and JWT access-token claims. - Resource-server JWT access-token verifier core for Rust integrations. It
validates
typ=at+jwt, issuer, audience, expiry, scopes, algorithm/key selection, and optional DPoP or mTLScnfsender constraints before application policy hooks run.
Nazo Auth Server is published in the OpenID Foundation certification listings.
The certified deployment is Nazo Auth Server 0.1.0, dated 09-Jun-2026:
- Certified OpenID Provider profiles
- Certified FAPI 2.0 OP Security Profile Final and Message Signing Final
Durable conformance records live in Git because GitHub Actions artifacts
expire. The certified deployment is backed by the 2026-06-09 OpenID Foundation
16-plan matrix against https://auth.nazo.run across OIDC Basic, OIDC Config,
FAPI2 Security Profile Final, FAPI2 Message Signing Final, mTLS, DPoP,
private_key_jwt, and client credentials variants. The latest official full
matrix reran the same public issuer through the real /ui/ frontend after the
OIDF-only interaction pages were removed and after JSON-only backend
authorization errors were enabled:
- 2026-06-09 OIDF full matrix
- 2026-06-13 real public UI OIDF regression
- 2026-06-14 local refactor OIDF full matrix
The latest recorded official workflow conclusion was success on run
27491182262 for commit
31c3d0665ec72ffb4babedfea519ed175ef403ad. The official runner reported 71
test modules, 6375 successes, 0 failures, and 0 warnings. The records
include commit SHA, run environment, plan IDs, exported artifact filenames,
profile combinations, artifact digest, and pass counts.
.
├── Cargo.toml
├── Containerfile
├── compose.yml
├── docs/
│ ├── conformance/
│ └── deployment.md
├── migrations/
├── scripts/
└── src/
├── bootstrap/ # application assembly and route registration
├── bin/ # operational commands
├── domain/ # domain rows, OAuth payloads, and settings types
├── http/ # endpoint handlers
├── support/ # shared security, storage, response, and protocol helpers
└── main.rs # HTTP service entry point
Key binaries:
| Binary | Purpose |
|---|---|
nazo-oauth-server |
HTTP authorization server |
nazo-oauth-migrate |
Database migration command |
nazo-oauth-keyctl |
JWT signing key lifecycle command |
- Rust toolchain compatible with edition 2024
- PostgreSQL 18 or compatible PostgreSQL server
- Valkey 8 or compatible Redis protocol server
- Docker or Podman for containerized local integration
Create a local configuration file:
cp .env.yaml.example .env.yamlStart the local integration stack:
docker compose up -d nazo_oauth_serverCheck the service:
curl -fsS http://127.0.0.1:8000/health
curl -fsS http://127.0.0.1:8000/.well-known/openid-configurationFor a direct host run, point DATABASE_URL and VALKEY_URL in .env.yaml at host-reachable services, then run:
cargo run --bin nazo-oauth-migrate
cargo run --bin nazo-oauth-serverConfiguration precedence is:
defaults < .env.yaml < process environment variables
Only explicitly allowlisted environment variables are accepted. A .env file is deliberately unsupported; if .env exists, the service refuses to start. Use .env.yaml for local or deployment configuration and do not commit real secrets.
Common settings:
| Setting | Default | Notes |
|---|---|---|
BIND |
0.0.0.0:8000 |
HTTP listener |
DATABASE_URL |
postgresql://postgres:postgres@127.0.0.1:5432/oauth |
PostgreSQL connection string |
VALKEY_URL |
redis://127.0.0.1:6379/0 |
Valkey connection string |
ISSUER |
http://127.0.0.1:8000 |
Must match discovery and token issuer exactly; production must use HTTPS |
FRONTEND_BASE_URL |
http://127.0.0.1:3000 |
Login and consent frontend base URL |
CORS_ALLOWED_ORIGINS |
http://127.0.0.1:3000 |
Comma list or YAML array |
DEFAULT_AUDIENCE |
resource://default |
Default access-token audience |
AUTHORIZATION_SERVER_PROFILE |
oauth2-baseline |
oauth2-baseline, fapi2-security, or fapi2-message-signing-authz-request |
COOKIE_SECURE |
derived from issuer | Must be true in HTTPS production |
TRUSTED_PROXY_CIDRS |
empty | Required before trusting forwarded IP or mTLS headers |
CLIENT_IP_HEADER_MODE |
none |
none, forwarded, or x-forwarded-for |
SUBJECT_TYPE |
public |
public or pairwise |
PAIRWISE_SUBJECT_SECRET |
empty | Required when SUBJECT_TYPE=pairwise |
EMAIL_DELIVERY |
disabled |
smtp enables registration email delivery |
AVATAR_STORAGE_DIR |
runtime/avatars |
Avatar storage path |
JWK_KEYS_DIR |
runtime/keys |
Signing key storage path |
SIGNING_EXTERNAL_COMMAND |
empty | Optional comma-separated argv for a KMS/HSM signing command or sidecar |
SIGNING_EXTERNAL_TIMEOUT_MS |
2000 |
External signer timeout in milliseconds |
See .env.yaml.example for the complete field list.
AUTHORIZATION_SERVER_PROFILE=fapi2-security requires PAR, confidential
clients, private_key_jwt or mTLS client authentication, sender-constrained
access tokens, and authorization code lifetimes of 60 seconds or less.
fapi2-message-signing-authz-request adds signed request objects at PAR.
Discovery metadata is generated from the active profile and mTLS proxy
configuration. mTLS capabilities are not advertised unless
TRUSTED_PROXY_CIDRS is configured.
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Health check |
GET |
/authorize |
Authorization endpoint |
GET |
/authorize/consent |
Consent page data |
POST |
/authorize/decision |
Consent decision |
POST |
/par |
Pushed Authorization Request |
POST |
/token |
Token endpoint |
GET/POST |
/logout |
OIDC RP-Initiated Logout |
POST |
/revoke |
Token revocation |
POST |
/introspect |
Token introspection |
GET |
/.well-known/openid-configuration |
OIDC discovery |
GET |
/.well-known/oauth-authorization-server |
OAuth server metadata |
GET |
/jwks.json |
JWKS |
GET |
/userinfo |
OIDC UserInfo |
The token endpoint accepts the RFC 8707 resource parameter as an absolute URI
without a fragment. A request may repeat resource to request multiple
audiences; single-resource access tokens keep a string aud, and
multi-resource access tokens use a JWT aud array. The legacy audience
parameter remains available as a single-audience project extension. A request
must not send both.
The authorization endpoint, PAR endpoint, and signed request objects accept RFC
9396-style authorization_details arrays. Each item must be an object with a
supported type; the server advertises account_information and
payment_initiation in authorization_details_types_supported. High-risk
details such as payments or write actions require fresh transaction binding and
are not silently covered by a previous broad consent.
OIDC logout is available at /logout and advertised as end_session_endpoint.
RP-Initiated Logout accepts id_token_hint, client_id,
post_logout_redirect_uri, and state; post-logout redirects require exact
registration in post_logout_redirect_uris. Registered clients with
backchannel_logout_uri receive best-effort back-channel logout notifications
signed as logout+jwt tokens.
Generate keys:
nazo-oauth-keyctl generate
nazo-oauth-keyctl generate --alg RS256
nazo-oauth-keyctl generate --alg ES256
nazo-oauth-keyctl generate --alg PS256Activate a new key after it has been deployed and published in JWKS:
nazo-oauth-keyctl activate <kid>Retire an old key after the maximum token TTL has elapsed:
nazo-oauth-keyctl retire <old-kid> --at 2026-06-01T00:00:00ZValidate the keyset:
nazo-oauth-keyctl validateRegister a non-exportable KMS/HSM key by storing only its public JWK and provider reference:
nazo-oauth-keyctl register-external \
--kid rs256-kms-2026-06 \
--alg RS256 \
--key-ref kms://prod/oauth/rs256-kms-2026-06 \
--public-jwk /secure/exported-public-jwk.json
nazo-oauth-keyctl validate
nazo-oauth-keyctl activate rs256-kms-2026-06When the active key uses backend: external-command, configure SIGNING_EXTERNAL_COMMAND to the signer argv. The signer receives JSON on stdin with kid, alg, key_ref, and the compact-JWS signing_input, and returns {"signature":"<base64url-signature>"} on stdout. Signing failures return protocol server_error; the server does not fall back to unsigned tokens or plain query responses.
The keyset uses atomic file replacement. On Unix platforms, private key PEM files are written with 0600 permissions. Retired keys are not published in JWKS, and the active key cannot be retired.
Run the standard local gates:
cargo fmt --check
cargo check
cargo clippy -- -D warnings
cargo test --lockedRun local Rust coverage with cargo-llvm-cov:
cargo install cargo-llvm-cov
cargo test --workspace --all-features
printf '%s\n' 'Generate lcov.info with the same target set as CI:'
cargo llvm-cov clean --workspace
eval "$(cargo llvm-cov show-env --sh)"
cargo clean
cargo test --workspace --all-features --lib --test oidf_seed --test resource_server
cargo llvm-cov report --lcov --output-path lcov.info \
--ignore-filename-regex '(^|/)(tests?|benches|examples|migrations)(/|\.rs$)|src/(schema|db)\.rs$|src/domain/rows\.rs$|src/bootstrap/routes\.rs$|src/support/valkey\.rs$|src/main\.rs$|src/bin/nazo_oauth_(keyctl|migrate|seed_oidf)\.rs$'
printf '%s\n' 'Generate an HTML report from the same coverage data:'
cargo llvm-cov report --html \
--ignore-filename-regex '(^|/)(tests?|benches|examples|migrations)(/|\.rs$)|src/(schema|db)\.rs$|src/domain/rows\.rs$|src/bootstrap/routes\.rs$|src/support/valkey\.rs$|src/main\.rs$|src/bin/nazo_oauth_(keyctl|migrate|seed_oidf)\.rs$'Coverage is used as a security signal, not a cosmetic target. Codecov is
configured for an 80% project target and a 90% patch target so changes improve
meaningful coverage without forcing artificial tests for generated schema,
migrations, examples, benches, test sources, Diesel row projection structs,
connection-pool glue, route table wiring, thin Valkey command wrappers, binary
entry wrappers, or local OIDF seed tooling. Protocol handlers, token
issuance/validation, client authentication, PKCE, DPoP, mTLS, PAR/JAR/JARM,
resource-server verification, repository state transitions, settings
validation, and OAuth/OIDC error mapping must not be excluded.
Test files are excluded from coverage accounting so split-out tests measure
production-code coverage rather than inflating totals with test implementation
lines.
Integration tests live directly under tests/*.rs. Unit tests that must access
private or pub(crate) implementation boundaries live under tests/unit/src/**
and are mounted from the owning module with #[cfg(test)]; this keeps test
source out of src/ without widening production APIs just for tests.
Security-critical protocol logic such as authorization-code exchange, PKCE,
client authentication, DPoP, mTLS, JWT/JWK validation, refresh-token rotation,
and OAuth error mapping should use behavior-oriented tests with exact error and
state assertions.
Run deterministic HTTP and race-condition checks:
python scripts/full_real_request_e2e.py
python scripts/full_real_request_load.pyThe conformance-security GitHub Actions workflow runs format, check, clippy,
tests, a real HTTP matrix, load/race checks, and a Valkey outage injection
check for implementation-affecting changes.
It also runs the supply-chain gate: cargo audit, cargo deny, CycloneDX SBOM
generation, container build, and Trivy image scanning. Tagged v* releases run
the separate release-security workflow for release binaries, SBOM artifact
upload, keyless artifact signing, and GitHub provenance attestations.
The full OIDF workflow is .github/workflows/oidf-conformance-full.yml. It runs the official OpenID Foundation Conformance Suite runner against a public HTTPS deployment and exports per-plan result archives.
Required GitHub secret:
OIDF_CONFORMANCE_TOKEN
Plan configuration can be provided either as OIDF_PLAN_CONFIG_JSON or chunked gzip+base64 secrets named OIDF_PLAN_CONFIG_JSON_GZ_B64_01 through OIDF_PLAN_CONFIG_JSON_GZ_B64_10.
Common variables:
| Variable | Default |
|---|---|
OIDF_CONFORMANCE_SERVER |
https://www.certification.openid.net/ |
OIDF_CONFORMANCE_SUITE_REF |
master |
OIDF_EXPORT_RESULTS |
true |
OIDF_VERBOSE |
true |
OIDF_DISABLE_SSL_VERIFY |
false |
OIDF_RUN_TIMEOUT_SECONDS |
14400 |
OIDF_MONITOR_INTERVAL_SECONDS |
60 |
The pass condition is stricter than a triggered workflow: GitHub Actions must conclude success, all suite plans must finish with 0 failures and 0 warnings, and the durable record under docs/conformance must be updated before the artifact expires.
Production deployment requires HTTPS, stable issuer metadata, PostgreSQL backups, Valkey availability, key rotation, strict trusted-proxy configuration, and live endpoint verification. See docs/deployment.md.
The implementation enforces these boundaries:
- Exact issuer, redirect URI, PKCE, client, and token binding checks.
- Refresh token rotation and token-family reuse detection.
- DPoP and mTLS sender-constrained token paths.
- Replay prevention for authorization codes, DPoP proofs, client assertions, and request objects.
- ASCII-safe OAuth protocol error descriptions.
- No-store token and protocol error responses.
- Explicit trusted proxy configuration before forwarded headers are trusted.
Refresh-token rotation for non-FAPI compatibility profiles is documented in docs/refresh-token-rotation.md. FAPI2 Security deployments do not use routine rotation by default; refresh grants still require confidential client authentication and the configured DPoP or mTLS proof, and newly issued access tokens remain sender-constrained.
The default deployment is single-tenant with tenant-aware schema boundaries. TOTP MFA, WebAuthn/passkeys, external OIDC/SAML federation, SCIM provisioning with hashed/scoped/audited database tokens, and Rust resource-server middleware are implemented. Dynamic Client Registration, Client Configuration Management, Device Authorization Grant, Token Exchange, and request-level multi-issuer tenant routing are outside the default scope; see docs/ecosystem-onboarding.md, docs/tenancy.md, and docs/oauth2-1-self-audit.md.
