key-cape/docs/authorization-code-bindings.md
tegwick 0d7e2f6b41 Document the authorization-code bindings for relying parties
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
2026-09-07 00:17:56 +02:00

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