CHANGELOG.md 10.0 KB

Changelog

All notable changes to the d-OAuth authorization server are recorded here.

The format follows Keep a Changelog, and the project uses Semantic Versioning. While the version stays below 1.0.0, a minor bump may carry a breaking change.

Unreleased

Added

  • Application-level envelope encryption for the PII directory: emails, client names, contacts, redirect URIs, JAR request URIs, JWKS URLs, software ids, and mTLS subject DNs/thumbprints are sealed with versioned AES-256-GCM under a new mandatory pii_encryption_key, and the looked-up or unique values carry a keyed-HMAC blind index. TOTP secrets move to a per-row envelope (blob version 3) so a DEK can be re-wrapped without re-encrypting the secret. New pii backfill, pii verify, and pii rotate commands run the one-shot migration and PII key rotation, and totp rewrap re-wraps the TOTP DEKs. The expansion adds _enc/_index columns; the backfill blanks the plaintext columns but this release does not drop them (the contract drop is a later release).
  • Documentation for the encryption model, the new keys, the expand/dual-write/scale-to-zero-backfill/cutover rollout, and the PII/TOTP rotation runbooks across docs/DATABASE.md, docs/SECURITY.md, docs/CONFIGURATION.md, and docs/OPERATIONS.md.
  • GET /logout (a server-specific session endpoint, not an OAuth 2.1 or OIDC feature) ends the browser session so a different user can sign in. It destroys the session and expires the cookie, then renders a neutral page or redirects to the flow's registered post_logout URI. It accepts no query parameters: the redirect target is frozen in session meta when the authorization flow is bound and used only while the owning client is active and still registers that exact URI (exact-string match, with the RFC 8252 loopback exception). There is no post_logout_redirect_uri parameter, no id_token_hint, and no end_session_endpoint in metadata.
  • The redirect_uris catalog gains a kind column (authorize | post_logout); existing rows backfill to authorize. redirect-uri create|enable|disable|delete and client attach|detach-redirect-uri accept --kind, and config sync file entries accept a kind field. A client may register at most one enabled post_logout URI.
  • The server-rendered pages (login, consent, device, TOTP) now share an embedded same-origin stylesheet served at GET /assets/app.css (src/ui/app.css). It adds no JavaScript and needs no CSP change: the page policy's existing style-src 'self' covers it, so script-src 'none' still holds.
  • TOTP enrollment now renders the otpauth:// URI as a server-side inline SVG QR code beside the text secret and URI, so users can scan instead of hand-typing. The new qrcode dependency is pinned SVG-only (no image codecs); the text secret, URI, and one-time recovery codes are unchanged.
  • A full-database-disclosure response procedure: docs/SECURITY.md gains the dump blast-radius table and the ordered rotation (token pepper, TOTP key, client secrets, DCR registration tokens), and RUNBOOK.md gains the matching operator checklist with concrete commands.
  • Cluster hardening in deploy/k8s: the token pepper and TOTP key mount as 0400 tmpfs files under /run/secrets (read through D_OAUTH_TOKEN_PEPPER_FILE / D_OAUTH_TOTP_ENCRYPTION_KEY_FILE, mirrored in compose.yaml), the image reference is digest-pinned, a NetworkPolicy default-denies pod egress except DNS plus Postgres/Redis scoped to their namespace (no internet-wide 0.0.0.0/0 peer; scripts/check_manifests.py rejects one), and automountServiceAccountToken is false. Remote JAR request_uri is documented as unsupported under the default-deny, with the Cilium FQDN opt-in path in docs/DEPLOYMENT.md.

Changed

  • The key ring now holds three independent chains (token pepper, TOTP encryption key, and PII field-encryption key), each with an optional retained previous key. pii_dual_write (default false, encrypt-only) turns on dual-write for the rolling-upgrade window; readiness fails closed while the switch is off and any blanked plaintext PII column still holds a value.
  • TOTP secrets now encrypt under HKDF-SHA256 (info totp-gcm-v2) instead of bare SHA256(totp_key). New blobs carry format version 2; version-1 blobs keep decrypting through a legacy fallback, so no re-enrollment is needed.

  • GET /.well-known/oauth-authorization-server now carries an explicit Cache-Control: public, max-age=3600 instead of relying on browser heuristic freshness, so metadata caching is intentional and tunable. The plaintext health listener (which does not mount the public router's security-header layer) now sets Cache-Control: no-store and X-Content-Type-Options: nosniff on its own probe responses, with a regression test pinning both.

Security

  • A PII or TOTP ciphertext that fails to open (tamper, truncation, or a missing key id) is fail-closed: the request returns 503 temporarily_unavailable and emits a pii.decrypt_failure audit event carrying only the request id, never a value. Full-blob substitution by an attacker with database write access remains out of the threat model and is documented in docs/SECURITY.md.

Fixed

  • GET /.well-known/oauth-authorization-server reads the enabled scope catalog from Postgres on every request. A scope added, enabled, or disabled via the CLI now appears in scopes_supported immediately on every replica, instead of only after a restart; the previous in-process cache was never invalidated by the out-of-process CLI mutation.
  • Responses on the OAuth surface are now Cache-Control: no-store (+ Pragma: no-cache) except /.well-known/… metadata, so authorization and consent pages, token bodies, request handles, and errors are never cached by a browser, the back/forward cache, or an intermediary. Token-endpoint error responses now carry no-store, closing a documented gap against draft-ietf-oauth-v2-1-16 §3.2.3.
  • GET /login?request_id=… rebinds an already-authenticated session to the fresh authorization request instead of redirecting on the session's stale pending binding.

0.1.0 - 2026-10-03

The first release. This is an OAuth 2.1-only authorization server, not a backwards-compatible OAuth 2.0 server. The deltas are deliberate: see OAUTH.md for the exact differences.

Added

  • Authorization-code grant with mandatory PKCE S256; plain and missing challenges are rejected. Implicit and resource-owner-password grants do not exist.
  • Refresh-token grant with rotation, tombstones, and reuse detection. Rotation is idempotent under a deterministic rotation_request_id, so a retried request returns the same tokens instead of double-rotating.
  • client_credentials, device-code (RFC 8628, with the user-facing /device page), and policy-gated token-exchange (RFC 8693) grants.
  • Opaque access and refresh tokens: 256 bits of CSPRNG output, base64url encoded, stored only as a keyed HMAC-SHA256 hash under a versioned pepper. No raw token ever reaches Postgres, Redis, a log, or an error body.
  • Dynamic client registration (RFC 7591) and management (RFC 7592), protected by a one-time initial-access token; open registration is off by default. Registration access tokens are hashed in Postgres so client management survives a Redis loss.
  • Sender-constrained tokens: DPoP (RFC 9449) and mTLS (RFC 8705), plus PAR (RFC 9126) and JAR (RFC 9101). private_key_jwt is client authentication only and is never treated as a sender constraint.
  • Introspection (RFC 7662) and revocation (RFC 7009), including grant-chain cascade revocation.
  • RFC 8414 authorization-server metadata with scopes_supported served from the enabled scopes table. The document carries no jwks_uri: the server signs no artifact of its own.
  • Resource-owner login with Argon2id, TOTP (RFC 6238, plus recovery codes and a used-code replay cache), server-side Redis sessions, and server-rendered login/consent pages with CSP and CSRF protection.
  • Runtime-managed scope catalog and redirect-URI catalog. Scope reduction is enforced as requested ∩ client-registered ∩ user-entitled ∩ enabled.
  • Operator CLI for every administrative action: migrate, serve, user, scope, redirect-uri, client, config sync, config check, admin bootstrap, initial-access-token issue, keygen, and healthcheck. There is no HTTP admin API and no default credential.
  • Admin bootstrap that creates exactly one admin under an advisory lock in a single transaction, with a forced password change and TOTP enrollment before the account gains authority.
  • Fail-closed dependency handling: a Postgres or Redis outage returns 503 temporarily_unavailable with Retry-After on security paths, never a fail-open success and never a 500 or panic.
  • A minimal multi-arch container image (static musl, non-root UID 65532, no shell) and a healthcheck subcommand that probes the plaintext health listener without a shell or curl.
  • Redis topologies from day one: single node, Sentinel (with hand-built master re-resolution), and Cluster (with native slot refresh). Multi-key atomic operations stay on one hash slot through the {state} hash tag.

Security

  • #![forbid(unsafe_code)] across the crate; unwrap, expect, and panic are denied by lint in request paths.
  • Opaque hash-at-rest token storage with a versioned key ring for the token pepper and the TOTP encryption key.
  • Rate limiting per IP and per client, with counters in a keyspace separated from token state so counters can never evict tokens.
  • Security headers: CSP with frame-ancestors 'none', X-Content-Type-Options, Referrer-Policy, and HSTS on the TLS listener.
  • No wildcard CORS. Browser public clients require an explicit origin allowlist.