Skip to content

feat(sep-1932): DPoP discovery signals (stacked on #528) - #529

Open
nbarbettini wants to merge 6 commits into
modelcontextprotocol:mainfrom
nbarbettini:feat/1932-server-dpop-discovery
Open

nbarbettini wants to merge 6 commits into
modelcontextprotocol:mainfrom
nbarbettini:feat/1932-server-dpop-discovery

Conversation

@nbarbettini

@nbarbettini nbarbettini commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #528 -> #527 -> #395 (PieterKas/conformance:dpop-server, head dd9fd77). This branch contains those commits plus 5d213b9. Rebase onto main once #528, #527, and #395 merge.

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

What each check asserts

#395 checks that a malformed proof is rejected. It does not check how a client that has never met the server learns that DPoP is required, or that the advertisement matches what the server enforces.

This assumes SEP-1932 will require the server to advertise DPoP - which @PieterKas proposed (and I agree with!). With that assumption, a missing DPoP challenge or a missing Protected Resource Metadata field is a failure. Missing algs stays a WARNING because RFC 9449 §7.1 says SHOULD.

sep-1932-server-advertises-dpop

RFC 9449 §7.1, graded SHOULD for algs from "An algs parameter SHOULD be included to signal to the client the JWS algorithms that are acceptable for the DPoP proof JWT. The value of the parameter is a space-delimited list of JWS alg (Algorithm) header values ([RFC7515], Section 4.1.1)."

An unauthenticated POST, no Authorization and no DPoP header.

  • 401 with a DPoP challenge that includes algs: SUCCESS
  • algs missing: WARNING
  • No DPoP scheme: FAILURE

The resource_metadata parameter is captured from the DPoP challenge, or from a sibling challenge when the DPoP one does not carry it (RFC 9728 §5.1).

sep-1932-server-prm-dpop

RFC 9728 §2 marks both fields OPTIONAL. Assuming SEP-1932 requires them, absence is a failure.

Fetched from resource_metadata when the challenge has it, otherwise the path-based well-known URL, otherwise the root one. Same order as the client metadata-discovery scenarios. A resource_metadata URL that does not return a document is not replaced by a different well-known document.

  • dpop_signing_alg_values_supported present, non-empty, and asymmetric only: the field half is satisfied. none or HS* is FAILURE, same rule as sep-1932-as-no-none-alg (RFC 9449 §11.6). Absent or empty: FAILURE
  • dpop_bound_access_tokens_required present as a boolean: the field half is satisfied. Absent: FAILURE
  • Metadata unreachable: not testable (FAILURE)

sep-1932-server-prm-consistency

An unbound token (omitCnf), presented as Bearer with no DPoP header. Gated on the positive proof.

  • Metadata says required and the server accepts: FAILURE
  • Metadata says required and the server rejects with 401: SUCCESS
  • Metadata says not required, or is silent, and the server rejects with 401: FAILURE ("server requires DPoP but does not advertise it")
  • Metadata says not required, or is silent, and the server accepts: INFO

Testing

  • DPoP server scenario tests pass (16), including the omit-fields, unbound-bearer, none, and missing-algs fixtures
  • eslint + prettier — clean on the touched files

Made with Cursor

PieterKas and others added 6 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
A crash or 404 must not count as correctly refusing a DPoP-bound token presented as Bearer.

Co-authored-by: Cursor <[email protected]>
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 <[email protected]>
A client that has never met the server has to learn the requirement from the 401 challenge and from protected resource metadata. A missing challenge or metadata field is a failure, and the advertisement has to match what the server enforces.

Co-authored-by: Cursor <[email protected]>
@nbarbettini
nbarbettini force-pushed the feat/1932-server-dpop-discovery branch from f20bf07 to 5d213b9 Compare September 25, 2026 18:39

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