feat(client): add DPoP (RFC 9449) sender-constrained token support - #2629
Open
gbshankar wants to merge 1 commit into
Open
feat(client): add DPoP (RFC 9449) sender-constrained token support#2629gbshankar wants to merge 1 commit into
gbshankar wants to merge 1 commit into
Conversation
Implements the client half of SEP-1932: DpopSession (key generation, proof building, nonce tracking), optional AuthProvider.authorizeRequest/ consumeChallenge and OAuthClientProvider.dpop() hooks, DPoP wiring in the token endpoint (auth.ts) and both transports (streamableHttp.ts, sse.ts), and a standalone withDpop() middleware. Opt-in via provider.dpop() - existing Bearer-only hosts are unaffected. Also wires the conformance harness client side: registers auth/dpop and auth/dpop-nonce against the pinned referee and removes them from expected-failures.yaml. Fixes a retry-ordering bug in withOAuth/withOAuthRetry found while verifying against the real referee: the DPoP nonce challenge must be re-checked *after* a credential re-authorization retry too, not only before it (auth/dpop-nonce's actual sequence), so both are now tried in whichever order the server presents them, bounded to one retry each. Verified against the pinned @modelcontextprotocol/conformance referee: auth/dpop 12/12, auth/dpop-nonce 14/14, full --suite all regression-clean.
🦋 Changeset detectedLatest commit: 9991200 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the client half of DPoP (RFC 9449) sender-constrained tokens per SEP-1932, a draft OAuth extension for MCP. This is the SDK-side work backing conformance#394 (merged), which added the
auth/dpopandauth/dpop-nonceclient scenarios that no SDK currently passes.DpopSession(newclient/dpop.ts): non-extractable keypair generation, RFC 9449 §4 proof building (freshjtiper proof, query/fragment-strippedhtu,athwhen presenting a token), and per-origin nonce tracking for both the AS and RS nonce challenges (§8/§9).AuthProvider.authorizeRequest?()/consumeChallenge?()— let a transport delegate per-request header construction and nonce-challenge handling instead of hard-codingBearer.OAuthClientProvider.dpop?()— a host returns aDpopSessionto opt in;adaptOAuthProviderthen presentsAuthorization: DPoP <token>+ a signedDPoPproof instead of Bearer, everywhere.auth.ts: proof on the token request, retry-once on AS400 use_dpop_nonce) and both transports (streamableHttp.ts,sse.ts: proof per request, retry-once on RS401 use_dpop_nonce).withDpop()middleware for hosts that build their ownfetchpipeline instead of usingOAuthClientProvider.auth/dpop/auth/dpop-nonceagainst the pinned referee (test/conformance/src/helpers/dpopClient.ts,everythingClient.ts) and removes both fromexpected-failures.yaml.Bug fix found while verifying against the real referee
withOAuth(and the conformance harness'swithOAuthRetry, same shape) only checked for a nonce challenge before the credential re-authorization retry, never after it.auth/dpop-nonce's actual sequence is: request with no token → 401 (no nonce yet) → re-authorize, get a token → request again → 401 again, this timeuse_dpop_nonce— which the old linear code never re-checked. Restructured both into a bounded while-loop (nonce retry and credential retry, each spent at most once, tried in whichever order the server presents them) — seemiddleware.tsand the conformance harness'swithOAuthRetry.ts. Covered by a new regression test shaped exactly like the scenario.Verification
packages/client/test/client/{dpop,auth.dpop,streamableHttp.dpop,sse.dpop,middleware.dpop}.test.ts), full client suite: 855/855 passing.pnpm --filter @modelcontextprotocol/client typecheck).@modelcontextprotocol/conformancereferee:auth/dpop12/12,auth/dpop-nonce14/14 (includingsep-1932-client-rs-nonce), full--suite allregression-clean.Activation
Fully opt-in: DPoP only activates when a host supplies a
DpopSessionviaOAuthClientProvider.dpop(). Hosts that don't implement it keep the exact prior Bearer-only behavior.Stack
This is part of a 3-PR DPoP rollout (mirroring the client/server split in the conformance repo's own
#394/#395):PR 2 and 3 links will be added here once opened. Not order-dependent with #2 — either can merge first.