feat(client): choose the callback binding in Config, not on the session store - #535
Merged
Merged
Conversation
…on store #532 let a session store declare storage.Capabilities.SingleUserAgent, which allowed a callback to complete with no SessionHandle. But "only one user agent writes this store" is a fact about where a store is deployed, not about its type. A store written for a mobile app and reused in a server-side web app would carry the declaration with it and silently bring login CSRF back. The declaration was also the only capability that relaxed a check rather than adding a requirement. It hasn't been released, so it moves now: - client.Config.CallbackBinding: CallbackBindingSessionHandle (the zero value, requiring AuthorizationCallback.Session as before) or CallbackBindingDeviceLocalStore (for a native app whose session store is its own on-device storage: the session is the callback's own state, taken from the verified signed response under Message Signing). It's an enum, set by whoever wires the client for a deployment, and New refuses an unknown value. - storage.Capabilities.SingleUserAgent is removed. - The missing-Session error no longer advertises turning the binding off. Its usual cause is a web client that forgot its cookie, so it points at the SessionHandle and client/sessioncookie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An authorization server's error code that isn't RFC 6749 error text was quoted into Error() at any length, up to the response size limit. It's now cut to 64 bytes, so only bounded text from the server reaches Error(). The client.Error type doc no longer calls PublicDescription safe to show a user: it can be the server's own error_description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
client's package doc gains a Native apps section: registration as a native app, RedirectPort, platform-keystore keys declared Durable, the on-device session store and its locking, completing after relaunch (CallbackBindingDeviceLocalStore, or a saved handle), TokenSetSealer, and mapping errors to Code() and ServerResponse() before they reach platform code. GETTING_STARTED points to it. client/doc.go and ARCHITECTURE no longer say the SessionHandle is always required. Dependencies.Sessions states its production requirements. KeyCustody.Durable now says a native key is kept until the tokens bound to it are discarded, not until the flow ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
osanderson
force-pushed
the
feat/client-callback-binding
branch
from
October 3, 2026 14:07
f0a718c to
e7720a6
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
These are the fixes from the security and DevX review of v0.45.0..fa120ab, before v0.46.0. There are three commits, for release-please.
feat(client): the callback binding moves toclient.Config(both reviews' main point).#532 let a session store declare
storage.Capabilities.SingleUserAgent, which let a callback complete with noSessionHandle. But "only one user agent writes this store" is a fact about where a store is deployed, not about its type:It hasn't been released, so it moves now:
client.Config.CallbackBinding:CallbackBindingSessionHandleis the zero value, unchanged behaviour:AuthorizationCallback.Sessionis required and compared.CallbackBindingDeviceLocalStoreis for a native app whose session store is its own on-device storage. The session is the callback's ownstate, read from the verified signed response under Message Signing.It's an enum (per the bool→enum rule, feat!: require explicit AuthorizationResponseIssPolicy #358), chosen per deployment by whoever wires the client.
Newrefuses an unknown value. Its doc says never to use it with a store a server shares between users, to keepSessionLifetimeshort (any pending session in the store can complete), and that savingHandle().String()on the device works too.storage.Capabilities.SingleUserAgentis removed.The missing-
Sessionerror said "required unless … declares SingleUserAgent", which told a web developer who forgot the cookie how to switch off their CSRF protection. It now points at theSessionHandleandclient/sessioncookie.fix(client): bounded error text.Error(), so only bounded server text reaches it.client.Errortype doc no longer callsPublicDescriptionsafe to show a user, since it can be the server's ownerror_description.docs:A "Native apps" section in
client's package doc ties together the native story from v0.44–v0.46:RedirectPort;keys.NewKeyManagerFromSigners+DeclareCustody(Durable);TokenSetSealer;Code()/ServerResponse()before gomobile turns them into an NSError.GETTING_STARTED links to it.
Stale statements fixed:
client/doc.goand ARCHITECTURE no longer say theSessionHandleis always required.Dependencies.Sessionsstates its production requirements.KeyCustody.Durablesays a native key is kept until the tokens bound to it are discarded, not until "the flow ends", which could be read as deleting the DPoP key at code exchange.Tests
TestCallbackWithoutSessionForADeviceLocalStore: completion with noSessionworks underCallbackBindingDeviceLocalStore, both plain and JARM.TestCallbackWithoutSessionStillRefused:Session;Sessionis refused.TestNewRefusesAnUnknownCallbackBinding.TestMissingSessionErrorPointsAtTheHandle: the error namessessioncookieand doesn't mention the device-local binding.TestPARErrorBoundsAMalformedCode: a 500-byte malformed code shows only its first 64 bytes.go test -raceacross client, storage, keys, fapitest and server passes.go test ./cmd/...passes.🤖 Generated with Claude Code