You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AgentAuthMiddleware accepts only the Bearer scheme (internal/middleware/agent_auth.go:70) and never reads cnf.jkt. #269 fixes the second half (require and validate a DPoP proof when the token is bound). This issue tracks the first half — accepting Authorization: DPoP — and, more importantly, the order in which the scheme change has to roll out relative to the SDKs, because RFC 9449 §7.2 turns a naive one-step fix into an outage for every SDK client that has opted into DPoP.
Why sequencing matters
highflame-sdk#105 adds dpop_key= to ZeroIDClient and Highflame. Until this server accepts the DPoP scheme, the SDK has to present bound tokens as
Authorization: Bearer <cnf.jkt-bound token>
DPoP: <proof with ath>
RFC 9449 §7.2 permits that against a Bearer-only resource server, and it is exactly the shape #269 validates. But §7.2 also says:
Protected resources simultaneously supporting both the DPoP and Bearer schemes ... MUST reject a DPoP-bound access token received as a bearer token.
So if we add DPoP-scheme support and §7.2 rejection in the same release, every deployed SDK client with dpop_key set starts getting 401s the moment the server upgrades. Spring Security and Keycloak already enforce §7.2, so the SDK cannot simply keep sending Bearer forever either.
SDKs: flip the auth-scheme constant from Bearer to DPoP and ship (Python, TS, Rust).
Server: enforce §7.2 — reject a cnf.jkt token presented as Bearer — behind a config flag (token.reject_bound_as_bearer or similar), default off. Flip the default once SDK versions have rolled.
Same sequence applies to /oauth2/token/verify (#272) once #274 settles the jti question, and to Shield (highflame-shield issue filed alongside this one).
Related gaps noticed while reading this path
internal/handler/wellknown.go:392 hardcodes dpop_bound_access_tokens_required: false in the PRM document, while the AS metadata (:208) reflects token.require_dpop. The two should agree.
No DPoP-Nonce is ever issued (§8). Okta and Bluesky require one, Auth0 requires one for public clients, and §11.2 explains the pre-generated-proof risk it closes. The SDKs are shipping the client-side retry now so the server can turn nonces on without another SDK release. Worth its own issue.
token.require_dpop is global; Keycloak and Okta hang the requirement on the client/app. A per-credential-policy setting would let one tenant require binding without forcing every SDK caller to opt in at once.
Summary
AgentAuthMiddlewareaccepts only theBearerscheme (internal/middleware/agent_auth.go:70) and never readscnf.jkt. #269 fixes the second half (require and validate aDPoPproof when the token is bound). This issue tracks the first half — acceptingAuthorization: DPoP— and, more importantly, the order in which the scheme change has to roll out relative to the SDKs, because RFC 9449 §7.2 turns a naive one-step fix into an outage for every SDK client that has opted into DPoP.Why sequencing matters
highflame-sdk#105 adds
dpop_key=toZeroIDClientandHighflame. Until this server accepts theDPoPscheme, the SDK has to present bound tokens asRFC 9449 §7.2 permits that against a Bearer-only resource server, and it is exactly the shape #269 validates. But §7.2 also says:
So if we add
DPoP-scheme support and §7.2 rejection in the same release, every deployed SDK client withdpop_keyset starts getting 401s the moment the server upgrades. Spring Security and Keycloak already enforce §7.2, so the SDK cannot simply keep sending Bearer forever either.Proposed sequence
BearerandDPoPschemes, case-insensitively (HTTP authorization schemes are case-insensitive (Bearer, DPoP) #256). Whenever the token carriescnf.jkt, require and validate the proof viaValidateBoundToTokenregardless of which scheme carried the token. Do not reject bound-as-Bearer yet. EmitWWW-Authenticate: DPoP algs="ES256 RS256"alongside the Bearer challenge on 401 (§7.1).BearertoDPoPand ship (Python, TS, Rust).cnf.jkttoken presented asBearer— behind a config flag (token.reject_bound_as_beareror similar), default off. Flip the default once SDK versions have rolled.Same sequence applies to
/oauth2/token/verify(#272) once #274 settles the jti question, and to Shield (highflame-shield issue filed alongside this one).Related gaps noticed while reading this path
internal/handler/wellknown.go:392hardcodesdpop_bound_access_tokens_required: falsein the PRM document, while the AS metadata (:208) reflectstoken.require_dpop. The two should agree.DPoP-Nonceis ever issued (§8). Okta and Bluesky require one, Auth0 requires one for public clients, and §11.2 explains the pre-generated-proof risk it closes. The SDKs are shipping the client-side retry now so the server can turn nonces on without another SDK release. Worth its own issue.token.require_dpopis global; Keycloak and Okta hang the requirement on the client/app. A per-credential-policy setting would let one tenant require binding without forcing every SDK caller to opt in at once.References
🤖 Generated with Claude Code