Skip to content

feat(sep-1932): tighten the Bearer-downgrade probe (stacked on #395) - #527

Open
nbarbettini wants to merge 7 commits into
modelcontextprotocol:mainfrom
nbarbettini:feat/tighten-dpop-bearer-downgrade
Open

nbarbettini wants to merge 7 commits into
modelcontextprotocol:mainfrom
nbarbettini:feat/tighten-dpop-bearer-downgrade

Conversation

@nbarbettini

@nbarbettini nbarbettini commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #395 (PieterKas/conformance:dpop-server, head dd9fd77). This branch contains that PR's commits plus dc35b8c. Rebase onto main once #395 merges.

The new commit (dc35b8c) is the only one to review.

What each check asserts

#395 covers DPoP proof validation on the MCP server. This change only tightens the existing bearer-scheme case only.

sep-1932-server-validate-proof (RejectsBearerScheme)

A DPoP-bound token presented under the Bearer scheme used to pass on any non-2xx. A server that crashes with a 500, or returns a 404, with no challenge was graded as correctly rejecting it.

401 with a WWW-Authenticate challenge whose scheme is Bearer (RFC 6750 §3) or DPoP is SUCCESS. The scheme is matched at the start of the header or after a comma, same as hasDpopChallenge. Any other status, or a 401 with no such challenge, is FAILURE.

The compliant fixture still reports SUCCESS. DPOP_BEARER_REJECT_STATUS=500 answers a Bearer-scheme request with that status and no challenge, and reports FAILURE.

This case is still filed under sep-1932-server-validate-proof, whose text is "MCP servers MUST validate DPoP proofs according to RFC 9449 Section 4.3". Refusing a bound token presented as Bearer is not a §4.3 step, and RFC 9449 §7.2 is descriptive only ("would most presumably accept"). SEP-1932 needs a normative sentence for this, and the check may deserve its own id once that sentence exists. No yaml text was added.

Testing

  • npm test — 46 files / 595 tests pass
  • npm run lint (eslint + prettier) — clean

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 server/dpop: require 401 + challenge for Bearer-scheme rejection feat(sep-1932): tighten the Bearer-downgrade probe (stacked on #395) Sep 25, 2026
Comment thread src/scenarios/server/auth/dpop.ts

@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.

One non-blocking comment for something that Claude (using Fable 5) flagged to prevent a future regression.

PieterKas added a commit to PieterKas/conformance that referenced this pull request Oct 1, 2026
Adds sep-1932-server-reject-bearer-downgrade to the traceability yaml,
quoting the normative sentence being added to the SEP-1932 extension
doc (MCP servers MUST NOT accept a DPoP-bound token presented under the
Bearer scheme; rejection is 401 + Bearer/DPoP challenge). The check
itself currently runs under sep-1932-server-validate-proof; upstream
PR modelcontextprotocol#527 tightens it and is the natural place to re-home it onto this
id, so the row links there and reports as untested until then.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PieterKas

Copy link
Copy Markdown
Contributor

@nbarbettini SEP-1932 now has a normative sentence for this case (modelcontextprotocol/ext-auth#33) and the conformance yaml in #395 declares sep-1932-server-reject-bearer-downgrade pointing at this PR. Since you're already editing this probe, would you re-home RejectsBearerScheme onto that id (including the error/catch path)? That flips the new traceability row from untested to tested.

nbarbettini and others added 3 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>
@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@527

commit: 248e957

@nbarbettini

Copy link
Copy Markdown
Contributor Author

@PieterKas Thanks for your review! I added the additional fixture mode and the traceability note.

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