Skip to content

SEP-1932: DPoP Profile for MCP - #1932

Open
PieterKas wants to merge 38 commits into
modelcontextprotocol:mainfrom
PieterKas:patch-1
Open

PieterKas wants to merge 38 commits into
modelcontextprotocol:mainfrom
PieterKas:patch-1

Conversation

@PieterKas

@PieterKas PieterKas commented Dec 5, 2025 •

Copy link
Copy Markdown
Contributor

Motivation and Context

This extension defines DPoP (Demonstrating Proof-of-Possession) support for the Model Context Protocol, enabling sender-constrained access tokens that prevent token replay attacks. This extension builds upon the baseline authorization requirements defined in the main Authorization specification.

DPoP binds access tokens to a cryptographic key pair controlled by the client. When accessing MCP server resources, clients must prove possession of the private key by providing a signed proof with each request. This prevents unauthorized use of tokens even if they are intercepted or leaked.

How Has This Been Tested?

TBD

Breaking Changes

No

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • [ x] New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

For inclusion as an auth extension

@D-McAdams

@PieterKas PieterKas changed the title Create 0000-dpop-extension.md 1932-dpop-extension.md Dec 5, 2025
@PieterKas PieterKas changed the title 1932-dpop-extension.md SEP-1932: DPoP Profile for MCP Dec 5, 2025
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@D-McAdams D-McAdams self-assigned this Dec 8, 2025
@D-McAdams D-McAdams added auth SEP draft SEP proposal with a sponsor. extension labels Dec 8, 2025
@D-McAdams
D-McAdams requested a review from a team December 8, 2025 16:27
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
PieterKas and others added 4 commits December 9, 2025 18:51
Co-authored-by: Brian Campbell <71398439+bc-pi@users.noreply.github.com>
Co-authored-by: Brian Campbell <71398439+bc-pi@users.noreply.github.com>
Co-authored-by: Brian Campbell <71398439+bc-pi@users.noreply.github.com>
Co-authored-by: Brian Campbell <71398439+bc-pi@users.noreply.github.com>
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
Comment thread seps/0000-dpop-extension.md Outdated
@sep-automation-bot

Copy link
Copy Markdown

Maintainer Activity Check

Hi @D-McAdams!

You're assigned to this SEP but there hasn't been any activity from you in 20 days.

Please provide an update on:

  • Current status of your review/work
  • Any blockers or concerns
  • Expected timeline for next steps

If you're no longer able to sponsor this SEP, please let us know so we can find another maintainer.


This is an automated message from the SEP lifecycle bot.

@oxcabe

oxcabe commented Aug 22, 2026 •

Copy link
Copy Markdown

Hi, I'm currently reviewing this SEP (including the dpop-extension draft) and the modelcontextprotocol/conformance#395, modelcontextprotocol/conformance#396 PRs. So far it's looking solid, although the conformance suite seems to be ahead of the spec. What I've found so far in terms of inconsistencies is:

There might be more inconsistencies, and since I don't have access to the https://ant-mcp-oss.slack.com Slack I'm not sure if any of these have already been discussed in there. In any case, I hope this helps 🙂

Comment thread seps/1932-dpop-extension.md Outdated

[RFC 9449 Section 11.1](https://www.rfc-editor.org/rfc/rfc9449.html#name-dpop-proof-replay) provides specific guidance on replay protection mechanisms that adress the risks of a DPoP Proofs being replayed.

MCP servers that is not capable of keeping state or perform global `jti` tracking provides DPoP proof replay protection by enforcing short `iat` acceptance windows of +/- 5 minutes and standard RFC 9449 claim validation. A stateless MCP server may provide additional replay protection by using a server supplied nonce as defined in [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) (e.g. by using an encrypted timestamp as the nonce value which can be decrypted, parsed and verified to be within an acceptable time window when returned in DPoP Proof) (e.g. by encrypting a timestamp using an Authenticated Encryption with Associated Data (AEAD) scheme which can be decrypted, parsed and verified to be within an acceptable time window when returned in DPoP Proof).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the nonce is implemented as an encrypted timestamp then that same nonce value will be valid for all clients that interact with a MCP server over a given time period. I can't immediately think of an attack that multi-client nonces would help enable:

  • Pre-generation is still impossible for the client since it cannot perform encryption of the nonce
  • Replay protection is unaffected - the nonce affects the server-accepted lifetime of the DPoP proof, not its ability to statefully track replay

Nonetheless, it strikes me as odd for the nonce used in sender-constraining tokens to clients to not itself be constrained to a client. Is this a problem worth solving? Am I overthinking it? One solution - a server could also encode the client ID in the nonce as well - ENC(CONCAT($client_id, "|",$timestamp)) - to create a unique nonce per client per time period.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank @max-stytch - It would be good to understand the threat you have in mind (designing countermeasures without defining the threat invites complexity without clear benefits).

To my knowledge this is not something that has come up for OAuth in general, which makes me hesitate on whether this is worth solving. Is there something about how MCP uses OAuth that would create a risk here?

Your idea is a clever way to make timestamped nonces unique and specific to a client when interacting with the Authorization Server. I don't believe the Resource Server in OAuth typically has knowledge of the client_id though. However, if timestamps are not sufficiently unique, it could be salted or additional information could be added (e.g. a session identifier if it exists etc) if a deployment needed it.

In terms of the specification, how the nonce is construsted is not an interoperability issue (only the server needs to know how to handle the nonce when it receives it back), so we can provide guidance, while still leaving implementers with options on how they implement the nonce.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @max-stytch - I don't think there are any attack surfaces from a multi-client nonce. It turns out RFC 9449 addresses this directly. Section 11.1 says "Unlike cryptographic nonces, it is acceptable for clients to use the same nonce multiple times and for the server to accept the same nonce multiple times," because the sender-constraint comes from each proof's key binding and freshness, not from nonce uniqueness. The nonce only bounds the proof's server-accepted lifetime, exactly as you described.

Your ENC(client_id | timestamp) construction works as optional hardening, and since the nonce is opaque to clients it costs nothing in interop. I've added text to the draft incorporating this idea in modelcontextprotocol/ext-auth#36.


[RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) already provides the core security properties for sender constraining OAuth Access Tokens. When combined with TLS transport-layer security and appropriate token and proof lifetimes, these mechanisms are sufficient to significantly reduce the risks associated with token exfiltration and replay in MCP deployments, even if the MCP server is not capable of maintaining global state.

## Backward Compatibility

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate a section explaining how MCP Servers and Client should work with WWW-Authenticate headers, for both DPoP + DPoP/Bearer backwards compatibility situations. 9449 describes the scheme but MCP makes heavy use of WWW headers for scope negotiation, and there are many existing client libraries that probably won't play well with MCP servers accepting both token types. I assume a header would look something like the following?

POST /mcp HTTP/1.1
Host: mcp.example.com

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer, DPoP algs="ES256 PS256",
    resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
    scope="files:read"

Showing how scope negotiation + PRM + DPoP upgrades work together in one place would be very helpful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @max-stytch - to what extent can we just rely on the PRM doc to advertise whether DPoP is required (or not) and what algorithms it supports rather than having a negotiation via headers? RFC 9728 already specifies both dpop_signing_alg_values_supported and dpop_bound_access_tokens_required (which we should make mandatory in this SEP)?

Is the case your thinking of specifically when a MCP server supports both DPoP and Bearer, and the client only supports one or the other?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@max-stytch this is now addressed in two places in modelcontextprotocol/ext-auth#33: a "MCP Server DPoP Advertisement" section making the PRM fields (dpop_signing_alg_values_supported, dpop_bound_access_tokens_required) the authoritative signal a client can act on before its first request, and a WWW-Authenticate Challenge section with exactly the dual-challenge shape you sketched (Bearer carrying scope and resource_metadata alongside a DPoP challenge carrying algs, extending RFC 9449 §7.2 Figure 17). Scope and resource_metadata ride the Bearer challenge, which is what existing MCP client libraries already parse.

@sep-automation-bot

Copy link
Copy Markdown

Maintainer Activity Check

Hi @D-McAdams!

You're assigned to this SEP but there hasn't been any activity from you in 20 days.

Please provide an update on:

  • Current status of your review/work
  • Any blockers or concerns
  • Expected timeline for next steps

If you're no longer able to sponsor this SEP, please let us know so we can find another maintainer.


This is an automated message from the SEP lifecycle bot.

@PieterKas

Copy link
Copy Markdown
Contributor Author

@oxcabe - thanks for your review - you are correct, the two documents have drifted and needs to be reconciled. Thanks for pointing to specific gaps. Much appreciated.

@sep-automation-bot

Copy link
Copy Markdown

Maintainer Activity Check

Hi @D-McAdams!

You're assigned to this SEP but there hasn't been any activity from you in 21 days.

Please provide an update on:

  • Current status of your review/work
  • Any blockers or concerns
  • Expected timeline for next steps

If you're no longer able to sponsor this SEP, please let us know so we can find another maintainer.


This is an automated message from the SEP lifecycle bot.

@sep-automation-bot

Copy link
Copy Markdown

Maintainer Activity Check

Hi @D-McAdams!

You're assigned to this SEP but there hasn't been any activity from you in 14 days.

Please provide an update on:

  • Current status of your review/work
  • Any blockers or concerns
  • Expected timeline for next steps

If you're no longer able to sponsor this SEP, please let us know so we can find another maintainer.


This is an automated message from the SEP lifecycle bot.

@eaakun

eaakun commented Oct 15, 2026

Copy link
Copy Markdown

Implementer perspective here (I maintain pydpop, a pure-RFC 9449 DPoP library, and have a token-endpoint DPoP PR open on the Python SDK) — two notes on the open threads above:

1. The token-endpoint profiling gap is the highest-value piece to fix first. oxcabe is right that the conformance suite tests token-request proofs and cnf.jkt/token_type binding while the draft text is silent. In practice the token endpoint is where DPoP does its most distinctive work (a stolen authorization code can't be redeemed without the proof key), and it's also the least specified — I had to derive our behavior strictly from RFC 9449 §4–§9 because the MCP-side text doesn't exist. Interop evidence: our implementation passes against Keycloak 26.8.0 (password grant, DPoP token requests, cnf.jkt binding — 6/6 checks, tampered proofs rejected), so RFC 9449's token-endpoint requirements are stable ground to profile from.

2. +1 on PRM-based advertisement (dpop_signing_alg_values_supported / dpop_bound_access_tokens_required, RFC 9728), mandatory. We implemented alg negotiation against exactly these metadata fields, and it's the one mechanism that avoids header-sniffing guesswork across heterogeneous MCP servers — which is also most of max-stytch's WWW-Authenticate coexistence concern. Making both mandatory in the SEP would let clients pick DPoP vs Bearer deterministically from the resource metadata before the first request.

Happy to contribute implementation experience (client + server sides, Python) if it helps close the acknowledged draft/SEP drift.

Clarify DPoP proof replay protection requirements for stateless MCP servers and specify the use of nonces.
Added section on server-provided nonces for DPoP proofs, detailing client requirements for nonce handling in token requests and responses.
Added section on obtaining DPoP-bound access tokens for MCP clients, specifying the requirement to include a DPoP proof in the token request.
@PieterKas

Copy link
Copy Markdown
Contributor Author

Thanks @eaakun. Implementer input with interop evidence is exactly what this needs, and the Keycloak results are encouraging.

On your two points: the token endpoint is now profiled in modelcontextprotocol/ext-auth#36, deliberately minimally. One normative sentence ("MCP clients conforming to this extension MUST obtain DPoP-bound access tokens by including a DPoP proof in the DPoP header of the token request") plus an applicability statement that token binding and signaling, refresh-token binding for public clients, and token-endpoint error handling apply as RFC 9449 Sections 5-6 specify, unmodified. Since you derived your implementation strictly from RFC 9449 §4-§9, that should confirm rather than change your behavior. RFC 9449 stays the normative home and the profile only states its one delta (that conforming clients use DPoP at the token endpoint at all).

On PRM advertisement: already in modelcontextprotocol/ext-auth#33 — dpop_signing_alg_values_supported is MUST in Protected Resource Metadata, and dpop_bound_access_tokens_required is MUST for servers that require DPoP, which gives clients the deterministic pre-request choice you described.

Reviews of both PRs are very welcome, as is the implementation experience you offered. The conformance suite (modelcontextprotocol/conformance) is where client/server implementations can be run against these requirements today.

@PieterKas

PieterKas commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

@oxcabe — following up with where each of your four items landed:

  1. RFC 9449 §7.2 Bearer downgrade: normative text is in DPoP: Update DPoP profile with server requirements from conformance implementation feedback ext-auth#33 ("Bearer Scheme Downgrade Protection"); the sep-1932.yaml row follows once it merges.
  2. Client-side nonce handling: both §8/§9 MUST-retry sentences are now in Harmonizing requriements across documents and the conformance suite ext-auth#36, word-for-word matching the conformance rows, with a note that the MUST is a profile tightening of RFC 9449's non-normative "the client is expected to retry".
  3. Token endpoint: profiled in PR 36 via one normative sentence (conforming clients MUST obtain DPoP-bound access tokens) plus an explicit statement that binding/signaling, refresh-token binding, and error handling apply per RFC 9449 §5-§6 unmodified; dpop_jkt is DPoP: Add authorization code binding recommendation (dpop_jkt) ext-auth#35 (SHOULD).
  4. iat window: harmonized in PR 36 — ±5 minutes recast as an outer bound (MUST reject outside it, MAY enforce narrower), the stateless clause now relies on that same bound, and the SEP markdown carries identical text, so the "/" concatenation in the sep-1932.yaml row can collapse to one quote after merge.

The conformance yaml updates are held in modelcontextprotocol/conformance#564, a draft PR that depends on these spec PRs merging first. Thanks again — this review is what triggered the reconciliation.

Pieter Kasselman and others added 2 commits October 16, 2026 20:21
Brings patch-1 up to date with main (which added the SEP docs generator)
and commits the generated docs/seps/1932-dpop-extension.mdx plus the
updated index and docs.json, fixing the render-seps check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PieterKas
PieterKas requested review from a team as code owners October 16, 2026 19:22
@figitaki

Copy link
Copy Markdown

@PieterKas (cc @nbarbettini) not-blocking, but I hit a protocol-version mismatch with the compliant example on conformance#529.

The scenario runs version 2026-07-28 and sends server/discover, however the example uses @modelcontextprotocol/sdk 1.29, which does not know about 2026-07-28.

AcceptsValidProof fails. 23 more checks fail as not testable, and ReportsDpopErrorCode warns as not testable. Only AdvertisesDpop and PrmDpop pass.

To reproduce, start sep-1932-compliant-server.ts with default settings and DPOP_ISSUER_JWK set to an ES256 public JWK, then run:

DPOP_ISSUER_PRIVATE_JWK='<private JWK JSON>' \
DPOP_ISSUER=https://conformance-dpop-issuer.example.com \
node dist/index.js server --url http://localhost:3010/mcp \
  --scenario auth/dpop-server-validation

The unit tests miss this because testContext() defaults to LATEST_SPEC_VERSION (2025-11-25), so they exercise initialize rather than the CLI’s default server/discover path.

Could we update the example’s SDK and add coverage at DRAFT_PROTOCOL_VERSION, or pin the scenario to 2025-11-25 until the example supports 2026-07-28? Reporting the version mismatch once would also make the failure easier to diagnose.

Separately, our resource-server connector passes 28/28 at both versions (--force on 2025-11-25). It uses the typescript-sdk#2630 branch, with server nonces and jti replay checks, so another passing implementation for the SEP!

@nbarbettini

Copy link
Copy Markdown
Contributor

@figitaki Thanks for testing! I recommend filing an issue on the conformance repo - if I understand correctly, some of the base fixtures need to be updated to TS SDK 2.x+ since it is now available. Things are moving fast!

@PieterKas

Copy link
Copy Markdown
Contributor Author

Thanks @figitaki — reproduced exactly as you described (2/26 at the default version, 27/27 pinned to 2025-11-25). The broken pairing turns out to be specific to conformance#529's new example servers: main's draft-introduced scenarios all pair with fixtures that answer server/discover before the SDK transport, which the new sep-1932 examples don't yet do. I've proposed fixing it within #529 — pre-transport handling in the three example servers, a unit test at the draft protocol version (your diagnosis of the testContext() default was exactly right), and failure output that names the version mismatch. There's also a latent version of the same problem in a few other example servers on main, which I'll raise as a separate hardening issue.

And thanks for the 28/28 report on your resource-server connector — a second independently passing implementation is exactly the evidence the SEP needs.

Generated with the aid of Claude (Fable 5).

This branch has not been deployed

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

Labels

auth draft SEP proposal with a sponsor. extension roadmap/security Roadmap (horizon): Security & Authorization security SEP

Projects

Status: Draft

Development

Successfully merging this pull request may close these issues.