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
{{ message }}
Repository navigation
DPoP sender-constrained tokens not implemented in OAuth client (spec mandates it) #3671
The MCP authorization spec requires DPoP sender-constrained tokens, but the Python SDK's OAuth client (src/mcp/client/auth/oauth2.py) implements no DPoP behavior: no proof JWT generation, no DPoP header on token requests, no client key management. The only DPoP references in the codebase are the server-metadata fields (dpop_signing_alg_values_supported, dpop_bound_access_tokens_required in src/mcp/shared/auth.py) — parsing what the server advertises, not acting on it.
Why it matters
CVE-2026-104850 (MCP SDK OAuth credential theft) demonstrated the exact failure mode this leaves open: bearer artifacts (authorization codes, refresh tokens) ending up in the wrong hands, where possession equals access. DPoP (RFC 9449) binds token requests to a client-held keypair, so a stolen authorization code can't be redeemed and a stolen token can't be replayed without the victim's private key. It doesn't prevent credentials from being sent to the wrong place (issuer validation is the primary fix) — it's the defense-in-depth backstop the spec already calls for.
What's available
I maintain pydpop — a pure-RFC 9449 DPoP library for Python (ES256 proof generation plus full server-side verification: htu normalization, iat window, jti replay protection, nonce, ath binding; one dependency, 37 tests). Happy to contribute DPoP support to the SDK's OAuth flow, or for pydpop to serve as a reference implementation.
I'd like to implement this — I have a working branch ready and can open a PR if you're open to an outside contribution here.
Approach (scoped deliberately small):
New mcp/client/auth/dpop.py: DPoP proof minting (ES256, embedded JWK, htm/htu binding), per-client P-256 key generation, server-nonce support. Zero new dependencies — built on pyjwt[crypto], which the SDK already requires.
OAuthClientProvider attaches the DPoP header to token requests (authorization_code exchange + refresh_token grant) when the protected resource metadata advertises dpop_signing_alg_values_supported containing ES256. Servers that don't advertise it get byte-identical requests.
Automatic single retry on a 400 + DPoP-Nonce challenge (RFC 9449 §9.1).
OAuthToken.token_type accepts "DPoP" (RFC 9449 §5 mandates it in token responses; currently rejected by the Literal["Bearer"]).
Out of scope for this PR (noted as follow-ups): DPoP proofs on resource-server requests, persisting the key in TokenStorage (currently in-memory per provider instance), reading the flag from authorization server metadata as well.
22 new tests, full suite green, ruff + pyright clean. The threat model this closes is exactly the stolen-code/refresh-token redemption from the CVE referenced above.
One correction on the premise, since it may change how this gets triaged. The 2025-11-25 authorisation spec does not mention DPoP anywhere (https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). What it does require is PKCE ("MCP clients MUST implement PKCE according to OAuth 2.1 Section 7.5.2") and Resource Indicators ("MCP clients MUST implement Resource Indicators for OAuth 2.0 as defined in RFC 8707"). On token theft, it says "Authorisation servers SHOULD issue short-lived access tokens to reduce the impact of leaked tokens."
DPoP is being proposed separately as SEP-1932, "DPoP Profile for MCP" (modelcontextprotocol/modelcontextprotocol#1932), which is still a draft extension. I think that SEP is the right place to argue for it, and SDK support would follow whatever shape it lands in.
Also, #3672 was closed by the bot for a missing issue link, so it may need a maintainer to pick up this issue first.
Activity
eaakun commentedon Oct 10, 2026
I'd like to implement this — I have a working branch ready and can open a PR if you're open to an outside contribution here.
Approach (scoped deliberately small):
mcp/client/auth/dpop.py: DPoP proof minting (ES256, embedded JWK,htm/htubinding), per-client P-256 key generation, server-nonce support. Zero new dependencies — built onpyjwt[crypto], which the SDK already requires.OAuthClientProviderattaches theDPoPheader to token requests (authorization_code exchange + refresh_token grant) when the protected resource metadata advertisesdpop_signing_alg_values_supportedcontainingES256. Servers that don't advertise it get byte-identical requests.400+DPoP-Noncechallenge (RFC 9449 §9.1).OAuthToken.token_typeaccepts"DPoP"(RFC 9449 §5 mandates it in token responses; currently rejected by theLiteral["Bearer"]).Out of scope for this PR (noted as follow-ups): DPoP proofs on resource-server requests, persisting the key in
TokenStorage(currently in-memory per provider instance), reading the flag from authorization server metadata as well.22 new tests, full suite green, ruff + pyright clean. The threat model this closes is exactly the stolen-code/refresh-token redemption from the CVE referenced above.
sattyamjjain commentedon Oct 12, 2026
One correction on the premise, since it may change how this gets triaged. The 2025-11-25 authorisation spec does not mention DPoP anywhere (https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). What it does require is PKCE ("MCP clients MUST implement PKCE according to OAuth 2.1 Section 7.5.2") and Resource Indicators ("MCP clients MUST implement Resource Indicators for OAuth 2.0 as defined in RFC 8707"). On token theft, it says "Authorisation servers SHOULD issue short-lived access tokens to reduce the impact of leaked tokens."
DPoP is being proposed separately as SEP-1932, "DPoP Profile for MCP" (modelcontextprotocol/modelcontextprotocol#1932), which is still a draft extension. I think that SEP is the right place to argue for it, and SDK support would follow whatever shape it lands in.
Also, #3672 was closed by the bot for a missing issue link, so it may need a maintainer to pick up this issue first.