Skip to content

DPoP at resource routes: accept the DPoP scheme and sequence RFC 9449 §7.2 rejection with the SDK rollout #322

Description

@rsharath

Summary

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.

Proposed sequence

  1. Server (this issue + fix(middleware): enforce DPoP sender-constraint on bound agent tokens #269): accept both Bearer and DPoP schemes, case-insensitively (HTTP authorization schemes are case-insensitive (Bearer, DPoP) #256). Whenever the token carries cnf.jkt, require and validate the proof via ValidateBoundToToken regardless of which scheme carried the token. Do not reject bound-as-Bearer yet. Emit WWW-Authenticate: DPoP algs="ES256 RS256" alongside the Bearer challenge on 401 (§7.1).
  2. SDKs: flip the auth-scheme constant from Bearer to DPoP and ship (Python, TS, Rust).
  3. 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.

References

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions