Skip to content

DPoP sender-constrained tokens not implemented in OAuth client (spec mandates it) #3671

Description

@eaakun

Summary

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.

Failure-mode write-up (three real token thefts, one missing control): https://github.com/eaakun/PyDPoP/blob/main/docs/ditto-oauth-theft-dpop-analysis.md

Activity

  1. added
    enhancementRequest for a new feature that's not currently supported
    on Oct 10, 2026
  2. eaakun commented on Oct 10, 2026

    @eaakun
    Author

    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.

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

    enhancementRequest for a new feature that's not currently supported

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions