Skip to content

feat(server): add DPoP (RFC 9449) proof validation - #2630

Open
gbshankar wants to merge 9 commits into
modelcontextprotocol:mainfrom
gbshankar:feat/dpop-server-validation
Open

feat(server): add DPoP (RFC 9449) proof validation#2630
gbshankar wants to merge 9 commits into
modelcontextprotocol:mainfrom
gbshankar:feat/dpop-server-validation

Conversation

@gbshankar

@gbshankar gbshankar commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

Adds server-side DPoP (RFC 9449 / SEP-1932) proof validation for MCP resource servers, hand-rolled on WebCrypto so it introduces no new dependency. It adds a requireDpopAuth gate mirroring the existing Bearer-auth gate (plus an Express adapter), and teaches verifyBearerToken to reject a DPoP-bound token sent under the plain Bearer scheme, as RFC 9449 requires.

MCP servers acting as OAuth 2.0 resource servers currently only support plain Bearer tokens. DPoP (RFC 9449, adopted by MCP as SEP-1932) lets a resource server cryptographically verify that whoever is presenting an access token also holds the private key it's bound to, closing the token-theft/replay gap plain Bearer tokens have. This completes the resource-server half, alongside the client-side support already merged in #2629.

How Has This Been Tested?

81 unit + integration tests, all passing locally (typecheck and lint clean for the touched packages):

  • dpop.ts: proof validation against RFC 9449 §4.3's checking order, with real generated WebCrypto keys across all 10 supported algorithms and RFC 7638 thumbprint calculation.
  • dpopAuth.ts: verifyDpopToken/requireDpopAuth, covering both a JWT-shaped and an opaque-string access token, the Bearer-scheme rejection path, scope/expiry enforcement, and the server-nonce retry flow.
  • bearerAuth.ts: the new DPoP-bound-token-rejected-under-Bearer branch.
  • @modelcontextprotocol/express's requireDpopAuth adapter: real listening-server HTTP round trips, not mocked requests.

Breaking Changes

None for existing callers. The one behavior change - verifyBearerToken rejecting a token under Bearer when its AuthInfo.cnf.jkt is set - only affects verifiers that opt into populating cnf.jkt, which didn't exist before this PR.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • 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

Part of a 3-PR DPoP rollout: client-side (#2629, merged), server-side proof validation (this PR), and a server-side conformance fixture stacked on top (#2631).

Implements the server half of SEP-1932: a hand-rolled WebCrypto DPoP
proof validator (packages/server/src/server/middleware/dpop.ts) covering
RFC 9449 Section 4.3's validation order and RFC 7638 JWK thumbprints
across all 10 asymmetric algs (ES256/384/512, RS256/384/512,
PS256/384/512, EdDSA) - no new dependency, keeps @modelcontextprotocol/
server runtime-neutral.

dpopAuth.ts adds verifyDpopToken/dpopAuthChallengeResponse/
requireDpopAuth mirroring the existing bearerAuth.ts trio, sharing the
WWW-Authenticate builder (extracted to authChallenge.ts) so Bearer and
DPoP challenges stay consistent. AuthInfo.cnf.jkt (core-internal) lets a
verifier surface RFC 9449 Section 6 token binding as a typed field.
verifyBearerToken now rejects a DPoP-bound token presented as Bearer
(RFC 9449 Section 7.1).

packages/middleware/express/src/auth/dpopAuth.ts adapts requireDpopAuth
for Express, reconstructing the request URI for htu from
req.protocol/host/originalUrl.

77 new unit + integration tests (real generated keys per algorithm, real
listening-server HTTP round trips for the Express adapter) - no unit
test synthesizes a fake CryptoKey.

Independent of the client-side DPoP PR (modelcontextprotocol#2629) - no shared files.
@changeset-bot

changeset-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4393c9b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@modelcontextprotocol/server Minor
@modelcontextprotocol/express Minor
@modelcontextprotocol/client Minor
@modelcontextprotocol/codemod Minor
@modelcontextprotocol/core Minor
@modelcontextprotocol/server-legacy Minor
@modelcontextprotocol/core-internal Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Aug 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2630

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2630

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2630

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2630

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2630

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2630

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2630

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2630

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2630

commit: 4393c9b

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
gbshankar and others added 3 commits August 24, 2026 20:40
Export DpopJwk so calculateJwkThumbprint's parameter type is documented,
and qualify cross-module @linkcode references that TypeDoc could not resolve
once the DPoP API surface was fully included.

Co-authored-by: Cursor <cursoragent@cursor.com>
@gbshankar
gbshankar marked this pull request as ready for review August 31, 2026 17:01
@gbshankar
gbshankar requested a review from a team as a code owner August 31, 2026 17:01
…n-sync

# Conflicts:
#	packages/core-internal/src/auth/errors.ts
…heme rule

The rule that a DPoP-bound token MUST be rejected when presented under the
Bearer scheme is defined in RFC 9449 §7.2 ("Compatibility with the Bearer
Authentication Scheme"), not §7.1 ("The DPoP Authentication Scheme"). Fixes
the citation in the changeset, the AuthInfo.cnf JSDoc, and the bearerAuth.ts
implementation comment.
verifyDpopProof checks the proof's jti claim for presence only; it never
tracks replay/uniqueness, and DpopAuthOptions has a first-class hook for a
server-provided nonce but no equivalent hook for jti-replay tracking. RFC
9449 §11.1 treats this as a SHOULD, defense-in-depth measure rather than a
strict requirement, so this doesn't change behavior — it just makes the
known gap visible in the code instead of only in review notes.
verifyDpopToken's ath/cnf.jkt handling is format-agnostic by design, but
every existing fixture used an opaque string ('tok-1', 'bound-token', etc.)
- nothing exercised an actual JWT-shaped access token, and bearerAuth.ts's
new "reject a DPoP-bound token presented as Bearer" branch had zero direct
tests. Add:

- verifyDpopToken cases using a real signed-JWT-shaped access token whose
  own payload carries cnf.jkt, decoded (not looked up) by the verifier -
  one accepting the matching-key case, one rejecting a mismatched key.
- a verifyBearerToken case confirming a token with cnf.jkt set is rejected
  under the Bearer scheme.
test/helpers/src/helpers/http.ts and oauth.ts (this package's own source,
not its tests) import vitest directly, since @modelcontextprotocol/test-helpers
is itself a shared testing-helper library consumed by other packages' test
suites. Pre-existing on main, surfaced by import/no-extraneous-dependencies
once the lint run got this far - unrelated to the DPoP changes above.
@gbshankar

Copy link
Copy Markdown
Contributor Author

@pcarleton this is ready for review

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

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant