Skip to content

feat(sep-1932): DPoP error codes (stacked on #527) - #528

Open
nbarbettini wants to merge 9 commits into
modelcontextprotocol:mainfrom
nbarbettini:feat/1932-server-error-code-get
Open

nbarbettini wants to merge 9 commits into
modelcontextprotocol:mainfrom
nbarbettini:feat/1932-server-error-code-get

Conversation

@nbarbettini

@nbarbettini nbarbettini commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #527 -> #395 (PieterKas/conformance:dpop-server, head dd9fd77). This branch contains those commits plus 9771ecb. Rebase onto #395 once #527 merges.

The new commit (9771ecb) is the only one to review here.

What each check asserts

#395 checks that a malformed proof is rejected with a DPoP challenge. It does not check which error the server reports. #527 tightens the Bearer-scheme probe so only a 401 with a Bearer or DPoP challenge counts.

sep-1932-server-error-code

RFC 9449 §7.1, graded SHOULD from "An error parameter ([RFC6750], Section 3) SHOULD be included to indicate the reason why the request was declined, if the request included an access token but failed authentication. The error parameter values described in [RFC6750], Section 3.1 are suitable, as are any appropriate values defined by extension. The value use_dpop_nonce can be used as described in Section 9 to signal that a nonce is needed in the DPoP proof of a subsequent request(s). Additionally, invalid_dpop_proof is used to indicate that the DPoP proof itself was deemed invalid based on the criteria of Section 4.3."

One check. The error auth-param is read from the DPoP challenge (the header may carry several), quoted or unquoted. Mismatches are listed in details. Absent or unexpected is WARNING. SUCCESS otherwise. Gated on the positive baseline. Existing MUST checks do not look at the code, so their status is unchanged.

  • Proof defects (tampered signature, missing claims, wrong typ, htm/htu mismatch, private key in jwk, stale or future iat, alg none or symmetric, duplicate header, malformed JWT, missing or wrong ath): invalid_dpop_proof
  • wrong-audience: invalid_token
  • cnf-jkt-mismatch and bearer-scheme: either

DPOP_ERROR_CODE_OVERRIDE=invalid_request returns that code on every 401. The check reports WARNING. The compliant fixture already emits invalid_dpop_proof for proof defects and invalid_token for token defects.

GET is not probed. Revision 2026-07-28 removed the GET stream, and a server that speaks only this revision SHOULD answer GET with 405, so a GET cannot show whether htm was checked. htm stays covered by the existing POST probe (RejectsHtmMismatch, proof htm of GET).

Testing

Made with Cursor

PieterKas and others added 4 commits September 9, 2026 18:52
…ocol#369)

Follow-up on the DPoP client PR (shared foundation). Adds the server-role
conformance for SEP-1932 / RFC 9449: the framework acts as a DPoP client
against the MCP server under test and emits the sep-1932-server-* checks
across the RFC 9449 §4.3 validation surface — proof validation, the ±5-minute
iat window, asymmetric-only algorithms, the 401 + WWW-Authenticate challenge,
token audience validation under DPoP, and the optional server-provided nonce.

- src/scenarios/server/auth/dpop.ts (+ test, spec-references): the scenario.
- examples/servers/typescript/sep-1932-{compliant,broken}-server.ts: passing
  and failing fixtures proving every check passes and fails.
- Registered in the pending + all-client scenario lists.

Depends only on the shared DPoP foundation (dpopProof/dpopToken); independent
of the authorization-server PR. No DPoP-capable MCP SDK exists yet, so
correctness rests on RFC vectors + an independent verifier + the fixtures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Refresh the held DPoP nonce from every response's DPoP-Nonce header
  (RFC 9449 §8.2 newest-wins) instead of capturing it once, so a server that
  rotates or single-uses its nonce no longer turns the negative probes into
  false "not testable" failures (or vacuous passes). (modelcontextprotocol#1)
- Widen the stale iat probe to -303 s (matching the future side) so the ±1 s
  quantization of the whole-second Date header can't pull it onto the ±300 s
  boundary and be false-accepted. (modelcontextprotocol#2)
- Update the isNonceChallenge comment and the untestable message: probes now
  carry the held nonce, so a use_dpop_nonce challenge is about the nonce
  lifetime (rotated/stale/single-use), not a missing nonce. (modelcontextprotocol#3)
- Fixture: parse integer env vars NaN-safely (intEnv helper) so a malformed
  DPOP_CLOCK_OFFSET_SECONDS / DPOP_IAT_SKEW_SECONDS falls back to its default
  instead of silently disabling the iat window. (modelcontextprotocol#4)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Soften the heldNonce comment: newest-wins refresh is correct for ROTATING
  servers; a strict single-use server that re-arms only via challenges can still
  push alternate negatives to untestable (correctly reported, not mis-scored) —
  the previous "rotate or single-use" over-claimed. (R5)
- Fix the clock-skew test comment: the stale probe is now -303s (~273s old),
  not -301s/~271s. (R5)

Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A crash or 404 must not count as correctly refusing a DPoP-bound token presented as Bearer.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nbarbettini nbarbettini changed the title feat(sep-1932): DPoP error codes and non-POST methods (stacked on #395) feat(sep-1932): DPoP error codes (stacked on #395) Sep 25, 2026
@nbarbettini
nbarbettini force-pushed the feat/1932-server-error-code-get branch from a727a7b to 9771ecb Compare September 25, 2026 17:35
@nbarbettini nbarbettini changed the title feat(sep-1932): DPoP error codes (stacked on #395) feat(sep-1932): DPoP error codes (stacked on #527) Sep 25, 2026
Comment thread src/scenarios/server/auth/dpop.ts
Comment thread src/scenarios/server/auth/dpop.ts Outdated
Comment thread src/scenarios/server/auth/dpop.ts Outdated
Comment thread src/scenarios/server/auth/dpop.ts Outdated
PieterKas added a commit to PieterKas/ext-auth that referenced this pull request Oct 1, 2026
Clarified error codes for DPoP proof validation failures and access token issues. See modelcontextprotocol/conformance#528
@PieterKas

Copy link
Copy Markdown
Contributor

FYI: SEP-1932 now specifies invalid_dpop_proof for any §4.3 validation failure, including malformed or duplicated DPoP headers (modelcontextprotocol/ext-auth#33), so the strict expectation for malformed-not-a-jwt / duplicate-dpop-header here is normatively backed rather than a judgment call.

Comment thread src/scenarios/server/auth/dpop.ts Outdated
Comment thread src/scenarios/server/auth/dpop.ts Outdated

@PieterKas PieterKas left a comment

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.

Overall looks good with a small number of suggested changes to consider.

nbarbettini and others added 5 commits October 4, 2026 08:53
The compliant fixture only challenged with DPoP, so a later edit could drop the Bearer arm of the check and the suite would still pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep both the DPoP server scenario registration and the SEP-2640 skills registrations in the pending scenario list.

Co-authored-by: Cursor <cursoragent@cursor.com>
SEP-1932 now requires rejecting a DPoP-bound token presented as Bearer, and the traceability row sep-1932-server-reject-bearer-downgrade names that requirement. The result path and the probe-error path both use that id.

Co-authored-by: Cursor <cursoragent@cursor.com>
A challenge with no error code was passing. The code is a SHOULD, so a wrong or missing value is a warning and the existing MUST checks stay as they are.

Co-authored-by: Cursor <cursoragent@cursor.com>
A §7.2 Bearer rejection and a 2xx acceptance were both warned as a missing DPoP error code. Read the code from the challenge that applies, and only when the server actually declined the request.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nbarbettini
nbarbettini force-pushed the feat/1932-server-error-code-get branch from 9771ecb to 770a6f4 Compare October 4, 2026 16:26
@pkg-pr-new

pkg-pr-new Bot commented Oct 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@528

commit: 770a6f4

@nbarbettini

Copy link
Copy Markdown
Contributor Author

FYI: SEP-1932 now specifies invalid_dpop_proof for any §4.3 validation failure, including malformed or duplicated DPoP headers (modelcontextprotocol/ext-auth#33), so the strict expectation for malformed-not-a-jwt / duplicate-dpop-header here is normatively backed rather than a judgment call.

@PieterKas Thanks for this! and for the good suggestions. Implemented them all and caught this PR up so it can merge cleanly.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants