Skip to content

bymoye/NazoAuth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

365 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nazo Auth Server

OpenID Certified

code-quality codeql dependency-review conformance-security oidf-conformance-full codecov

中文文档

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.

Project Map

Capabilities

  • 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, and PS256.
  • client_secret_basic, compatibility client_secret_post, private_key_jwt, public clients, and mTLS client authentication. High-security clients use private_key_jwt or mTLS rather than client_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 resource parameter support for token requests, including repeated resource indicators mapped to JWT access-token aud arrays. The legacy audience parameter is disabled by default and is available only when ENABLE_LEGACY_AUDIENCE_PARAM=true.
  • RFC 9396-style Rich Authorization Requests through authorization_details on authorization, PAR, and signed request object inputs when ENABLE_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 mTLS cnf sender constraints before application policy hooks run.

Certification And Conformance

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:

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:

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.

Architecture

.
├── 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

Local Requirements

  • 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

Local Start

Create a local configuration file:

cp .env.yaml.example .env.yaml

Start the local integration stack:

docker compose up -d nazo_oauth_server

Check the service:

curl -fsS http://127.0.0.1:8000/health
curl -fsS http://127.0.0.1:8000/.well-known/openid-configuration

For 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-server

Runtime Configuration

Configuration 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.

Endpoints

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.

Key Management

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_SECONDS has elapsed and the active key reaches SIGNING_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 validate

Register 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

When 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.

Local Checks

Run the standard local gates:

cargo fmt --check
cargo check
cargo clippy -- -D warnings
cargo test --locked

Run 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.sh

On 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.py

The 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.

OpenID Foundation Suite

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.

Deployment

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.

Security Boundaries

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.

About

OpenID Certified Rust OAuth 2.1 / OpenID Connect authorization server with FAPI 2.0, DPoP, mTLS, PAR, JAR, and PostgreSQL/Valkey persistence.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages