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