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 prepublished, active, grace, and retired keyset states.
- 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 legacyaudienceparameter is disabled by default and is available only whenENABLE_LEGACY_AUDIENCE_PARAM=true. - RFC 9396-style Rich Authorization Requests through
authorization_detailson authorization, PAR, and signed request object inputs whenENABLE_AUTHORIZATION_DETAILS=true. Supported detail types are then 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
- 2026-06-25 PR 13 security hardening OIDF full matrix
The latest recorded official full matrix was the 2026-06-25 PR 13
security-hardening run for commit
49467e3474b32c17603ed77ba63b570d07e794b2 against https://auth.nazo.run.
The official runner exported all 16 plan archives; every plan summary reported
0 failures and 0 warnings. A Hostinger-local suite run against the same
public issuer and commit also exported all 16 plan archives with 0 failures
and 0 warnings.
Baseline OIDC metadata advertises none for unsigned Request Object
compatibility. FAPI2, signed-authorization-request, PAR request-object, and
holder-bound-token paths continue to reject unsigned Request Objects fail
closed.
.
├── 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 |
ENABLE_REQUEST_OBJECT |
false |
Enables the authorization endpoint request parameter |
ENABLE_REQUEST_URI_PARAMETER |
false |
Enables the authorization endpoint request_uri parameter |
ENABLE_PAR_REQUEST_OBJECT |
false |
Enables signed request objects inside PAR requests |
ENABLE_AUTHORIZATION_DETAILS |
false |
Enables RFC 9396 authorization_details processing and metadata advertisement |
ENABLE_LEGACY_AUDIENCE_PARAM |
false |
Enables the non-standard token endpoint audience parameter |
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 |
SIGNING_KEY_ROTATION_INTERVAL_SECONDS |
7776000 |
Automatic signing key rotation interval |
SIGNING_KEY_PREPUBLISH_SECONDS |
86400 |
JWKS prepublication window before activation |
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 is a non-standard single-audience project extension and is rejected
unless ENABLE_LEGACY_AUDIENCE_PARAM=true. A request must not send both.
The authorization endpoint, PAR endpoint, and signed request objects accept RFC
9396-style authorization_details arrays only when
ENABLE_AUTHORIZATION_DETAILS=true. Each item must be an object with a
supported type; when enabled, 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.
Initial startup creates a local RS256 signing key when keyset.json does not
exist. Existing local PEM keysets are maintained by the in-process lifecycle
task and published through the runtime keyset snapshot:
- when the active key reaches
SIGNING_KEY_ROTATION_INTERVAL_SECONDS - SIGNING_KEY_PREPUBLISH_SECONDS, the service prepublishes the next local key in JWKS without using it for signing; - after
SIGNING_KEY_PREPUBLISH_SECONDShas elapsed and the active key reachesSIGNING_KEY_ROTATION_INTERVAL_SECONDS, the service activates the prepublished key; - the previous active key remains in JWKS until
now + max(ACCESS_TOKEN_TTL_SECONDS, ID_TOKEN_TTL_SECONDS), then it is no longer published.
Defaults are a 90-day rotation interval and 1-day prepublication window. The prepublication window must be positive and shorter than the rotation interval. The runtime refresh interval is derived from the prepublication window and is capped at one hour.
Validate 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 validateWhen the active key uses backend: external-command, configure
SIGNING_EXTERNAL_COMMAND to the signer argv. A registered external key can be
activated by the automatic lifecycle only after its prepublication window has
elapsed and only when the signer command is configured. 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. The active key cannot carry retire_at, and malformed lifecycle metadata
fails validation.
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
python -m pip install requests "psycopg[binary]" redis argon2-cffi pyjwt cryptography aiosmtpd
bash scripts/generate_codecov_lcov.shOn Windows, run coverage in Docker using docs/coverage/codecov-docker-runbook.md so PostgreSQL, Valkey, Python, and llvm-cov instrumentation stay in one repeatable environment.
Coverage is used as a security signal, not a cosmetic target. Codecov is
configured with a project baseline 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.
The coverage script starts disposable PostgreSQL and Valkey containers, runs the
real HTTP E2E matrix against an llvm-cov-instrumented server binary, runs the
Rust unit/integration coverage targets, and merges all profiles into lcov.info.
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.
