From 679e85bc01add5868b25930385c30c82b972237b Mon Sep 17 00:00:00 2001 From: Oscar Sanderson Date: Sun, 27 Sep 2026 22:44:10 +0800 Subject: [PATCH] docs: add UPGRADING.md covering breaking changes since v0.30.0 One page per breaking change from v0.30.0 to v0.38.0: who it affects, why it exists, and the code change needed, with snippets for the session-handle cookie binding, key custody declaration and X5CAttesterChain issuer binding. Linked from the README. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 +- UPGRADING.md | 200 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 UPGRADING.md diff --git a/README.md b/README.md index eb2acae5..b5def602 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,8 @@ role is tested against the OpenID Foundation conformance suite. See [CONTRIBUTING.md](CONTRIBUTING.md) for the conformance-first development philosophy and pre-PR checklist, [SECURITY.md](SECURITY.md) to report a -vulnerability, and [CHANGELOG.md](CHANGELOG.md) for release history. +vulnerability, [CHANGELOG.md](CHANGELOG.md) for release history, and +[UPGRADING.md](UPGRADING.md) for what to change when a release is breaking. ## License diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000..e239047d --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,200 @@ +# Upgrading + +This page covers every breaking change from v0.30.0 onward: who each one +affects and what to change. Releases with no breaking changes aren't +listed. [CHANGELOG.md](CHANGELOG.md) has the full history. + +`New` fails at startup when a new requirement isn't met, and the error +names the missing field, so a skipped step can't slip through to +runtime. If you're several versions behind, work through the sections +from oldest to newest. + +Most production-assurance changes only affect `Config.Assurance = +AssuranceProduction`. A development-assurance setup built on `memstore` +and `keys/ephemeral` needs only the steps not marked *production only*. + +## v0.38.0 + +### `Dependencies.Random` must be `crypto/rand.Reader` (production only) + +**Affects:** `server.New` and `client.New` under `AssuranceProduction` +with any other `Random`, including a wrapper around `crypto/rand.Reader`. + +**Why:** codes, tokens, `state`, `nonce`, PKCE verifiers and every `jti` +are only as unguessable as this reader, and an `io.Reader` can't declare +that it is a CSPRNG. + +```go +deps.Random = rand.Reader // crypto/rand +``` + +`resource.Dependencies.Random` is unchanged. + +### JOSE and metadata member names are case-sensitive + +**Affects:** nobody talking to a conforming peer. A JWS or JWE header, +JWK, JWK Set, DPoP proof, client assertion, client attestation, access +token `cnf` claim or AS metadata document whose member names only +case-fold to a known name (`"ALG"` for `"alg"`) is now rejected instead +of being read as that member. No code change is needed. + +## v0.37.0 + +### `AuthorizationCallback.Session` is required (client) + +**Affects:** every client using the browser flow +(`HandleAuthorizationResponse` or `CompleteAuthorization`). + +**Why:** a callback's `state` only identifies a session, and anyone can +deliver a callback URL to someone else's browser. Binding the callback to +the browser that started the flow stops login CSRF (RFC 9700 §4.7). + +Store the handle with the user agent when you redirect, and pass it back +at the callback: + +```go +// Starting the flow. +session, err := rp.BeginAuthorization(ctx, req) +if err != nil { /* ... */ } +http.SetCookie(w, &http.Cookie{ + Name: "fapi_session", + Value: session.Handle().String(), + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, // Lax, so the cookie survives the AS's top-level redirect back +}) +http.Redirect(w, r, session.URL().String(), http.StatusFound) + +// At the redirect URI. +cookie, err := r.Cookie("fapi_session") +if err != nil { /* reject: no session for this browser */ } +handle, err := client.ParseSessionHandle(cookie.Value) +if err != nil { /* reject */ } +result, err := rp.CompleteAuthorization(ctx, client.AuthorizationCallback{ + RawQuery: r.URL.RawQuery, + Session: handle, +}) +``` + +A callback whose `Session` doesn't match its `state` is rejected before +the session is consumed. Tests using `fapitest.RunAuthorizationCodeFlowWithCallback` +now pass the session handle as its second argument. + +### Signing and decryption keys must declare custody (production only) + +**Affects:** `server.Dependencies.Keys`, `JWTAccessTokens.Keys`, +`client.Dependencies.Keys` and `client.Dependencies.Decryption` under +`AssuranceProduction`. + +Declare how the keys are held when you build the key manager or +decrypter: + +```go +km, err := keys.NewKeyManagerFromSigners(signers, algorithms, kids, + keys.DeclareCustody(keys.KeyCustody{ + Durable: true, // keys survive a restart (KMS, HSM, or your own durable storage) + CrossInstanceConsistent: true, // every instance uses the same keys (needed with HorizontallyScaled) + })) +``` + +`keys.NewDecrypter` and `keys.NewSingleKeyDecrypter` take the same +option. A custom `KeyManager` or `Decrypter` implements +`keys.KeyCustodyAssurance` instead. `keys/ephemeral` never declares +custody and is rejected in production. + +## v0.36.0 + +### `X5CAttesterChain.IssuerBinding` is required (server) + +**Affects:** servers using `X5CAttesterChain` for attestation-based +client authentication. + +**Why:** a client attestation's `iss` is chosen by the attester. Without a +binding, any attester certified under a shared trust anchor could +authenticate as another attester's clients. + +```go +deps.AttesterTrust = server.X5CAttesterChain{ + TrustAnchors: server.StaticAttesterTrustAnchors{Roots: pool}, + IssuerBinding: server.AttesterIssuerInCertificate, +} +``` + +- `AttesterIssuerInCertificate`: the signing certificate carries a URI + SAN exactly equal to the client's `ExpectedAttesterIssuer`. Use this + whenever anchors are shared between attesters. +- `AttesterIssuerByTrustAnchors`: your `AttesterTrustAnchors` returns + anchors belonging to each client's attester alone, so the chain itself + identifies the attester. + +## v0.34.0 + +### `Dependencies.AttesterTrust` is required with attestation auth (server) + +**Affects:** servers with `Config.AttestationBasedClientAuthentication` +set. + +- To keep the previous behavior (attester key looked up by `kid` in + `ClientKeys`), pass `server.RegisteredAttesterKeys{}`. +- To verify the attestation's `x5c` certificate chain (HAIP 1.0 §4.4.1), + pass `server.X5CAttesterChain` with trust anchors and, from v0.36.0, + an `IssuerBinding` (see above). + +## v0.33.0 + +### `Limits.MaxIDTokenClaimsBytes` is required (server) + +**Affects:** servers building `Limits` by hand, unless `Config.OAuthOnly` +is set. `server.RecommendedLimits()` already sets it to 4096. + +### Stores persist an opaque `Request` or `Grant` value (storage) + +**Affects:** custom `TransactionStore`, `GrantStore` and +`BackchannelAuthenticationStore` implementations. `memstore` is already +updated. + +- Replace the removed per-field columns with the single `Request` or + `Grant` value, and persist and return it unmodified. The server owns + its format, so future fields never touch your schema. +- Run `storage.TestTransactionStoreContract`, `TestGrantStoreContract` + and `TestBackchannelAuthenticationStoreContract` against your store. +- Records written by an earlier version can't be read after upgrading. + Pushed requests, authorization codes and CIBA requests are + short-lived. Refresh tokens are not, so clients will need to + re-authorize once. + +## v0.31.0 + +### `AuthorizationResponseIssPolicy` is required (client) + +**Affects:** every client. `Config.RequireAuthorizationResponseIss` +(bool) is replaced by `Config.AuthorizationResponseIssPolicy`, with no +default: + +- `client.RequireAuthorizationResponseIss` matches the former `true`. +- `client.TolerateAbsentAuthorizationResponseIss` matches the former + `false` or unset. + +### Key sources must declare `KeySourceAssurance` (production only) + +This one wasn't flagged as breaking in the v0.31.0 changelog. + +**Affects:** `server.Dependencies.ClientKeys`, `ClientEncryptionKeys` +and `client.Dependencies.IssuerKeys` under `AssuranceProduction`. A +custom key source implements `keys.KeySourceAssurance` and declares +`LiveFetchHardened`: every live fetch goes through `fapihttp`, or there +is no live fetch at all. `keys.JWKSIssuerKeySource` and +`keys.NewLocalIssuerKeys` already declare it. + +## v0.30.0 + +### `ClientCertificateTrust` replaces `MTLSClientCAs` (server) + +**Affects:** every server. `Dependencies.MTLSClientCAs` is renamed to +`Dependencies.ClientCertificateTrust` and is now required: + +- `server.TrustedClientCAs{Roots: pool}` if you previously set + `MTLSClientCAs` (this package verifies the chain). +- `server.NoClientCertificateChainTrust{}` if you left it unset and + verify client certificate chains yourself (for example, at a + TLS-terminating proxy).