Repository navigation
Conversation
Revised the DPoP extension documentation to clarify the purpose, scope, and requirements for sender-constrained access tokens in the Model Context Protocol. Enhanced explanations of DPoP proof requirements, content digest calculations, and replay protection mechanisms.
Added section on Bearer Scheme Downgrade Protection, specifying that DPoP-bound access tokens must not be used as bearer tokens and detailing the server's response requirements.
Clarified error codes for DPoP proof validation failures and access token issues. See modelcontextprotocol/conformance#528
Added DPoP advertisement section for MCP servers, including requirements for `dpop_signing_alg_values_supported` and `dpop_bound_access_tokens_required` fields in Protected Resource Metadata.
Added details on MCP server DPoP advertisement and requirements for DPoP-bound access tokens.
Removed extra line break in the DPoP challenge section.
Clarify requirements for DPoP challenge in WWW-Authenticate header.
Adds a SHOULD for MCP clients to bind the authorization code to their DPoP key per RFC 9449 Section 10. RFC 9449 makes this OPTIONAL; this profile recommends it as defense in depth alongside MCP’s mandatory PKCE. Surfaced by the conformance work in modelcontextprotocol/conformance#523; kept separate from #33 so the two can be discussed and merged independently.
Clarified DPoP proof validation requirements and replay protection mechanisms for MCP servers, emphasizing the rejection of invalid 'iat' claims and the use of nonces for enhanced security.
Added section on server-provided nonces in DPoP proofs, detailing requirements for authorization and MCP servers regarding nonce handling and client behavior.
Added section on obtaining DPoP-bound access tokens, detailing requirements for MCP clients to include DPoP proof in token requests.
Clarified nonce usage and replay protection mechanisms for DPoP Proofs, including guidance on client binding and stateless server requirements.
Add example challenge for server supporting dual authentication schemes with scope and resource metadata.
Move Pieter Kasselman's DPoP profile from specification/draft/ to specification/experimental/dpop.mdx and list it in README.md. The only changes to the text are the ones Prettier makes, so the format check passes.
|
I'm implicitly approving this as experimental given the amount of work and conversation we've had about it. Note that in SEP-3392 terms, experimental is for prototypes, and the move to
|
| "authorization_endpoint": "https://auth.example.com/authorize", | ||
| "token_endpoint": "https://auth.example.com/token", | ||
| "dpop_signing_alg_values_supported": ["ES256", "PS256"] | ||
| } |
There was a problem hiding this comment.
In a recent discussion with @ThisIsMissEm we noticed that there is no AS Metadata parameter to indicate that DPoP is required by an AS. There's a DCR Metadata param, a RS Metadata param, but no AS Metadata param. Should we propose one to IANA? (very much outside the scope of this PR)
There was a problem hiding this comment.
Hi @max-stytch , what is the use case you have in mind where an AS would require DPoP?
Generally I think that requiring DPoP is a resource server decision, not an authorization server decision.
The RS declares the requirement in its Protected Resource Metadata (dpop_bound_access_tokens_required) and enforces it by rejecting bearer tokens with a 401 that carries a DPoP challenge in WWW-Authenticate (RFC 9449 Section 7.1/7.2). That rejection sends the client back to the AS to obtain a DPoP-bound token.
The AS advertises support through dpop_signing_alg_values_supported (Section 5.1), and when the client includes a DPoP proof in the token request it issues a bound token and signals this with token_type: DPoP (Section 5).
If an AS needs to enforce DPoP for a specific client, RFC 9449 already defines the dpop_bound_access_tokens client registration parameter (Section 5.2). So the AS supports DPoP but does not mandate it. The requirement comes from the RS so not sure we really need a new AS metadata parameter.
|
|
||
| ### Error Response Example | ||
|
|
||
| When validation fails, the MCP server returns an error response: |
There was a problem hiding this comment.
Would also <3 an error response showing the nonce
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="use_dpop_nonce",
error_description="Fresh nonce required in DPoP proof",
algs="ES256 PS256"
DPoP-Nonce: eyJ7S_zG.eyJH0-Z.HX4w-7v
Content-Type: application/json
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized"
},
"id": 1
}
There was a problem hiding this comment.
Should this error include resource_metadata and/or scope args?
There was a problem hiding this comment.
@max-stytch - I see you have an example that includes a nonce - Is that the one you want to include here?
|
|
||
| ``` | ||
| HTTP/1.1 401 Unauthorized | ||
| WWW-Authenticate: DPoP error="invalid_dpop_proof", |
There was a problem hiding this comment.
Should this error include resource_metadata and/or scope args?
| - Deployments where token theft risk is elevated. | ||
| - Compliance requirements mandating proof of possession or sender constrained tokens. | ||
|
|
||
| This extension uses DPoP as defined in [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449), without introducing MCP-specific claims, request body digests, or additional proof material. This design choice reflects a deliberate trade-off in favor of interoperability, deployability, and alignment with existing OAuth 2.0 security models. It does not preclude the use of the extension mechanisms defined in [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449). |
There was a problem hiding this comment.
Should we be more specific about the trade-off somewhere in the spec? Namely htm and htu are always the same for MCP servers, which makes it possible that a DPoP proof can be replayed if the RS doesn't check jti and the proof is still valid.
There was a problem hiding this comment.
@max-stytch I proposed a "security cosniderations" section that goes into this at length.
|
I snuck in a review! Don't consider any of my comments blocking - happy to iterate on main. |
There was a problem hiding this comment.
+1 to landing this as experimental. Our TypeScript resource server passes all 28 checks of auth/dpop-server-validation (modelcontextprotocol/conformance#529). These notes come from building against this text.
- Before merge: the file doesn't compile as MDX, and two examples contradict the text. Suggestions are inline.
- Non-blocking: my two #33 points (403 step-up, and
algsvs PRM) didn't carry over. I re-linked them inline so they survive #33 closing. Settling them onmainis fine.
Drafted with AI assistance. I checked each point against the file and our implementation.
| [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 are NOT required to track `jti` values, however, MCP servers MAY choose to implement `jti` tracking for additional security, following the guidance in [RFC 9449 Section 11.1](https://datatracker.ietf.org/doc/html/rfc9449#section-11.1). | ||
|
|
||
| MCP servers that are not capable of keeping state or performing global `jti` tracking MUST rely on the `iat` acceptance window above (±5 minutes or narrower) together with standard RFC 9449 claim validation as their DPoP proof replay protection. 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). For example, the MCP server may create a nonce by encrypting a timestamp using an Authenticated Encryption with Associated Data (AEAD) scheme such as defined in [RFC 7518 Section 5.2](https://www.rfc-editor.org/rfc/rfc7518#section-5.2) which can be decrypted, parsed and verified to be within an acceptable time window when the MCP client returns the DPoP Proof to the MCP server). | ||
|
|
||
| A nonce constructed this way is valid for any client during its time window. This is consistent with [RFC 9449 Section 11.1](https://datatracker.ietf.org/doc/html/rfc9449#section-11.1), which permits the same nonce value to be used by multiple clients and accepted multiple times. Replay protection comes from each DPoP proof's key binding and freshness, not from nonce uniqueness. A server MAY nonetheless bind the nonce to a client, for example by encrypting a client identifier together with the timestamp, as additional hardening. The nonce is opaque to clients, so this choice has no interoperability impact. |
There was a problem hiding this comment.
| [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 are NOT required to track `jti` values, however, MCP servers MAY choose to implement `jti` tracking for additional security, following the guidance in [RFC 9449 Section 11.1](https://datatracker.ietf.org/doc/html/rfc9449#section-11.1). | |
| MCP servers that are not capable of keeping state or performing global `jti` tracking MUST rely on the `iat` acceptance window above (±5 minutes or narrower) together with standard RFC 9449 claim validation as their DPoP proof replay protection. 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). For example, the MCP server may create a nonce by encrypting a timestamp using an Authenticated Encryption with Associated Data (AEAD) scheme such as defined in [RFC 7518 Section 5.2](https://www.rfc-editor.org/rfc/rfc7518#section-5.2) which can be decrypted, parsed and verified to be within an acceptable time window when the MCP client returns the DPoP Proof to the MCP server). | |
| A nonce constructed this way is valid for any client during its time window. This is consistent with [RFC 9449 Section 11.1](https://datatracker.ietf.org/doc/html/rfc9449#section-11.1), which permits the same nonce value to be used by multiple clients and accepted multiple times. Replay protection comes from each DPoP proof's key binding and freshness, not from nonce uniqueness. A server MAY nonetheless bind the nonce to a client, for example by encrypting a client identifier together with the timestamp, as additional hardening. The nonce is opaque to clients, so this choice has no interoperability impact. | |
| DPoP provides sender-constrained access tokens, but does not guarantee that a valid DPoP proof can only be used once. [RFC 9449 Section 11.1](https://www.rfc-editor.org/rfc/rfc9449.html#section-11.1) describes the risk of proof replay and the available mitigations. | |
| **Replay risks in MCP:** DPoP binds a proof to an HTTP method (`htm`) and target URI (`htu`), but not to the HTTP request body. Because MCP uses JSON-RPC over HTTP, different operations commonly `POST` to the same endpoint. Consequently, `htm` and `htu` provide important replay protection generally, but only limited replay protection between different MCP operations. | |
| An attacker who obtains both a valid DPoP proof and its associated access token could reuse them within the proof's validity window, potentially with a different JSON-RPC request body. Without additional replay detection, the server cannot distinguish this reuse from a legitimate request. | |
| MCP servers can address these risks with increasing levels of protection: | |
| 1. **Sender-constrained tokens** (baseline): DPoP itself prevents the use of a stolen access token without a corresponding valid proof. | |
| 2. **Stateless replay mitigation:** Short validity windows and server-provided nonces reduce the opportunity to reuse a captured proof. | |
| 3. **Stateful replay prevention:** Tracking previously accepted proof identifiers (`jti`) can prevent a captured proof from being accepted more than once. | |
| **Stateless replay mitigation:** MCP servers MUST enforce the proof validity window and all other DPoP validation requirements defined above. Servers MAY enforce a narrower validity window or use server-provided nonces, as described in [RFC 9449 Sections 9 and 11.1](https://www.rfc-editor.org/rfc/rfc9449.html#section-11.1), to further limit replay opportunities. | |
| For example, a stateless MCP server can issue a nonce containing an encrypted timestamp, using authenticated encryption such as AEAD ([RFC 7518 Section 5.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2)). When the client returns the nonce in a DPoP proof, the server can decrypt it and verify that it falls within an acceptable time window, without maintaining per-request state. | |
| Such nonces may be reused by multiple clients and accepted multiple times, as permitted by RFC 9449. Servers MAY additionally bind nonces to a particular client. However, neither a short validity window nor a reusable server-provided nonce guarantees that an individual DPoP proof is accepted only once. | |
| **Stateful replay prevention using `jti`:** MCP servers MAY track previously accepted DPoP proof identifiers (`jti`) and reject proofs that have already been used, as described in [RFC 9449 Section 11.1](https://www.rfc-editor.org/rfc/rfc9449.html#section-11.1). This provides stronger replay protection, but requires coordination across all server instances that may receive the same proof. Tracking `jti` independently on each instance does not guarantee replay prevention in horizontally scaled deployments. | |
| Implementers should evaluate the replay risk based on the sensitivity of the operations their MCP server exposes. Servers handling sensitive or non-idempotent operations are encouraged to implement coordinated `jti` tracking when the additional operational cost is justified. | |
| Servers that do not implement single-use proof enforcement should recognize that their protections limit the opportunity for replay rather than preventing replay entirely. |
There was a problem hiding this comment.
@PieterKas This is my suggested prose update based on the conversation with @max-stytch and @figitaki above. Feel free to disagree or amend.
Tldr - I don't think we should upgrade jti from MAY to SHOULD, but I do think we should clearly spell out the tradeoffs following from RFC 9449 Section 11.1.
There was a problem hiding this comment.
@nbarbettini - I think we are mixing normative sections for how DPoP operates and is implemented with what is effectively security considerations. I would instead propose that the risks are discussed in security considerations and we keep the previous text here which talks about the mechanics of DPoP.
There was a problem hiding this comment.
I will propose a "security considerations" section that we can forward reference from this section that describes the computational mecchanisms.
There was a problem hiding this comment.
@PieterKas Sounds good, I like the idea of talking about the risks (security considerations) separately from mechanics. I mainly wanted to convey that different mitigations are appropriate depending on the risks, and separately push back on the idea of jti as a SHOULD (although it is very useful and important for high-security cases).
There was a problem hiding this comment.
I will mark this comment as resolved if the security considerations section below works for folks.
|
|
||
| ### Stateless Operation and DPoP Proof Replay Protection {#stateless-replay-protection} | ||
|
|
||
| [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. |
There was a problem hiding this comment.
As discussed further down, adding a forward reference to security considerations (and fixing some typos).
| [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. | |
| [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 address the risk of a DPoP proof being replayed. The replay risks specific to MCP, and guidance on choosing among the available mitigations, are discussed in [DPoP Proof Replay](#dpop-proof-replay) under Security Considerations. |
| The ±5 minute bound balances security and usability. MCP servers MAY enforce a narrower window within it, but should be aware that overly restrictive windows may cause operational issues due to clock skew in distributed systems. | ||
|
|
||
| For high-security environments, MCP servers SHOULD implement server-provided nonces as described in [RFC 9449 Section 9](https://datatracker.ietf.org/doc/html/rfc9449#section-9) to further limit proof lifetime, even if it is stateless (see {{stateless-replay-protection}}). | ||
|
|
There was a problem hiding this comment.
Below the proposed security considerations that outlines the risk and provides guidelines on additional mitigations. Once accepted, we might further refine it by bringing the "Proof Validity Window" security considerations into this section as well.
@nbarbettini @figitaki @max-stytch - please review and provide suggestions or a thumbs up if you are good with this addition.
| ### DPoP Proof Replay {#dpop-proof-replay} | |
| DPoP-bound access tokens provide stronger protection than bearer tokens. A stolen bearer token can be presented by anyone, from anywhere, for its entire lifetime. A stolen DPoP-bound token is unusable without a valid proof signed with the client's key. The residual risk is replay of the proof itself. | |
| #### Replay Risk in MCP | |
| A DPoP proof binds to the HTTP method (`htm`) and target URI (`htu`), but not to the request body. MCP operations commonly `POST` to the same endpoint, so these claims provide little isolation between operations. An attacker who captures both a valid proof and its associated access token can replay them within the proof validity window, potentially with a different JSON-RPC request body. Without additional replay detection, the MCP server cannot distinguish this reuse from a legitimate request. | |
| #### Minimizing Proof Capture | |
| Replaying a proof first requires capturing one. The baseline MCP Authorization specification requires TLS in accordance with [OAuth 2.1 Section 1.5](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-1.5), which protects proofs and access tokens in transit between the MCP client and MCP server. Capture risks remain at the endpoints and at intermediaries. Proofs can appear in request logs, be captured by TLS-terminating proxies or traffic inspection systems, or be exfiltrated by malware. These channels expose proofs in the same way they expose access tokens. The mitigations below limit what an attacker can do with a captured proof. | |
| #### Levels of Protection | |
| How strongly an individual proof is protected against reuse depends on which of the mitigations described in [RFC 9449 Section 11.1](https://www.rfc-editor.org/rfc/rfc9449.html#section-11.1) a server deploys. MCP servers can address the risk with increasing levels of protection: | |
| 1. **Proof validity window** (baseline, required by this profile). Limits how long a captured proof remains usable (see [Proof Validity Window](#proof-validity-window)). | |
| 2. **Stateless replay mitigation.** Narrower validity windows and reusable, time-limited server-provided nonces further limit when a captured proof can be replayed, without requiring server-side state (see [Stateless Operation and DPoP Proof Replay Protection](#stateless-replay-protection)). | |
| 3. **Single-use proof enforcement.** Strict per-request nonces or `jti` tracking prevent a captured proof from being accepted more than once. | |
| #### Replay Protection with Nonces | |
| A nonce is chosen by the server. The server controls its value, its lifetime, and how many times it is accepted. The level of protection a nonce provides is therefore determined by implementation choices. Each choice balances replay risk against implementation complexity. The choices can be combined to reduce risk. Nonces are opaque to clients, which already handle `use_dpop_nonce` retries, so these choices do not affect interoperability. | |
| - **Acceptance count.** At the strict end, the server issues a unique nonce per request and accepts each nonce exactly once, so every request carries a fresh proof. This provides single-use proof enforcement with a server-chosen identifier. The server must record which nonces it has accepted. This state is short-lived because a nonce is tracked only for its validity period. At the lenient end, a nonce is accepted multiple times. The server stays stateless, but a captured proof remains replayable while its nonce is accepted. | |
| - **Nonce lifetime.** A nonce can carry its own lifetime, for example as an encrypted timestamp. At the strict end, the server sets a nonce lifetime shorter than the proof validity window, creating a server-controlled replay window. At the lenient end, a nonce remains valid for the full proof validity window. | |
| Any combination of these choices can be implemented without extra round trips. [RFC 9449 Sections 8.2 and 9](https://www.rfc-editor.org/rfc/rfc9449.html#section-8.2) define how a server supplies the next nonce in the `DPoP-Nonce` header of a successful response. Issuing one single-use nonce at a time limits concurrent requests from the same client. | |
| #### Replay Protection with `jti` Tracking | |
| As an alternative to strict nonces, a server can track accepted proof identifiers (`jti`) and reject proofs that have already been used. In contrast to a nonce, a `jti` value is chosen by the client. The server cannot control its form or scope it in advance, so it must coordinate `jti` tracking across all server instances that may receive the same proof. | |
| #### Client Binding | |
| Client binding strengthens either mechanism by scoping acceptance to the presenting client. With nonces, the server encodes a client or instance identifier in the nonce and accepts the nonce only from that client. Because the server chooses the value, acceptance can be checked within that scope without coordination across server instances. With `jti` tracking, the server keys its store by the presenting client, for example by client identifier or by the proof key thumbprint. This shrinks the tracking domain and allows it to be partitioned. If requests from a client are routed to the same instance, acceptance checks remain local. Since `jti` values are client-chosen, coordination is still required wherever one client's requests reach multiple instances. | |
| #### Choosing a Mitigation | |
| Choosing among these mitigations is a risk-based decision. Implementers should weigh the sensitivity of the operations their MCP server exposes against the operational cost of each mitigation. Servers handling sensitive or non-idempotent operations benefit most from single-use proof enforcement through strict per-request nonces or coordinated `jti` tracking. Servers exposing low-risk, idempotent operations may reasonably rely on the proof validity window alone. In every configuration, a DPoP-bound access token provides stronger protection than a bearer token. | |
Co-authored-by: Max Gerber <[email protected]>
Co-authored-by: Max Gerber <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Co-authored-by: Carey Janecka <[email protected]>
Brings @PieterKas's DPoP profile to
specification/experimental/dpop.mdx, as an Experimental extension under SEP-3392. The proposal is SEP-1932.pieterkas-dpop-extensionand the heads of DPoP: Update DPoP profile with server requirements from conformance implementation feedback #33, DPoP: Add authorization code binding recommendation (dpop_jkt) #35 and Harmonizing requriements across documents and the conformance suite #36, keeping all 18 of his commits.README.md.@PieterKas feel free to push final tweaks here before it merges. Experimental carries no stability promise, so the rest can be settled on
main. The more serious review milestone comes before promotion tobeta.