KEY-WP-0016 changed /token and /userinfo behaviour with no consumer-facing note; nothing in docs/ mentioned redirect_uri, so the change would have reached a deployment silently. States what an exchange must now send, who is affected and how to roll out. Every browser registration in dev-config is public with an authorization_code grant, so only the redirect_uri requirement can affect them; the realistic failure is a client that sends it to /authorize and omits it at /token, which has not been observed against a live consumer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P Assistant: claude-code Assistant-Model: opus Assistant-Process: 713576@bnt-lap001 Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
3.4 KiB
Authorization-code bindings and rollout note
KEY-WP-0016 added four bindings to the /token authorization-code grant and
tightened /userinfo verification. Three of them can change the outcome of a
request that previously succeeded, so this note states exactly what a relying
party must send. It describes source behaviour; the deployed issuer only enforces
it once an image built from that revision or later is rolled out.
What the exchange must now send
| Requirement | Behaviour | Effect on a conforming client |
|---|---|---|
redirect_uri on the token request |
Must be present and byte-identical to the value sent to /authorize for that code. Comparison is exact — no normalization, no trailing-slash tolerance. |
None. RFC 6749 section 4.1.3 already requires it whenever /authorize carried one, and KeyCape's /authorize always does. |
| Grant-type eligibility | A client whose registration lists grantTypes without authorization_code is refused on this path. An absent or empty grantTypes still means an authorization-code client, matching config validation. |
None for the registrations in config/dev-config.yaml, which all declare authorization_code. A client_credentials-only client could previously reach this path and can no longer. |
| Confidential client authentication | A client registered as clientType: confidential must present its secret via client_secret_basic. A confidential registration with no configured secret is refused rather than accepting an empty one. |
None for the current browser clients, which are all public. Relevant to the planned human approval registration if it is registered confidential. |
| Single-use codes | A code is consumed atomically at lookup, so exactly one concurrent exchange can succeed and a failed exchange does not leave a replayable code. | None for a client that exchanges each code once. A client that retried a failed exchange with the same code now gets authorization code not found or expired. |
/userinfo additionally requires alg: RS256 in the JOSE header, an iss equal
to this issuer, and an access token — an ID token presented as a bearer token is
now rejected. Present the access token, not the ID token.
Who this affects
Every browser client in config/dev-config.yaml — openbao-admin, demo-app,
netkingdom-bootstrap-console, user-engine-portal and coulomb-social — is
clientType: public with grantTypes: ["authorization_code"], so only the
redirect_uri requirement can affect them. Standard OIDC client libraries send
it, and the in-repo keycape login command already did, so no consumer change is
expected.
The realistic failure is a client that sends redirect_uri to /authorize but
omits it at /token, which some libraries allow when a single redirect URI is
registered. That now returns HTTP 400 with feature redirect_uri. This has not
been observed against a live consumer; it is the failure mode to look for first
if a login that worked before the rollout starts failing at the token step.
Rolling out
The change is in the issuer only — no registration, secret, key or claim changes,
and no configuration migration. Roll out the image and watch for 400s carrying
feature redirect_uri or grant_type, and 401s carrying feature
Authorization, on /token. Rolling back is a straight image revert; nothing
persisted changes shape.
Evidence for each binding is in src/internal/server/oidc/hardening_test.go.