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