All notable changes to the d-OAuth-Server 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.
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).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.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.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.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.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.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.user disable now purges all Redis state for the subject (sessions and their
meta, codes, devices with their sidecars and mappings, grants via cascade
revoke, standalone tokens, and the lockout counter) before flipping
users.disabled; a Redis failure aborts before the flag changes, so the
command fails closed.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.
Forwarded client-cert and IP headers are never trusted; the dead proxy-header fallback is removed.
The login lockout now also covers TOTP enrollment confirmation, and the rate limiter classifies requests by their full path.
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.users.disabled, and refresh re-checks
the subject's current entitlements; a disabled subject introspects inactive
and refresh rejects with invalid_grant before any token is minted.WeakKeyMaterial.private_key_jwt assertion lifetime is capped and its jti replay marker
now tracks the remaining validity; PS256 is added to the assertion
algorithm allowlist.open redirect policy now accepts only https or RFC 8252 loopback
URIs, and the SSRF guard pins the fetch to the addresses it pre-validated.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.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.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.
S256; plain and missing
challenges are rejected. Implicit and resource-owner-password grants do not
exist.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.private_key_jwt is client authentication
only and is never treated as a sender constraint.scopes_supported served from
the enabled scopes table. The document carries no jwks_uri: the server
signs no artifact of its own.requested ∩ client-registered ∩ user-entitled ∩ enabled.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.503 temporarily_unavailable with Retry-After on security paths, never a
fail-open success and never a 500 or panic.healthcheck subcommand that probes the plaintext health
listener without a shell or curl.{state} hash tag.#![forbid(unsafe_code)] across the crate; unwrap, expect, and panic
are denied by lint in request paths.frame-ancestors 'none', X-Content-Type-Options,
Referrer-Policy, and HSTS on the TLS listener.