Skip to content

fix(client): validate protected resource metadata identity - #3169

Open
bbdu3 wants to merge 2 commits into
modelcontextprotocol:mainfrom
bbdu3:fix/prm-resource-validation
Open

bbdu3 wants to merge 2 commits into
modelcontextprotocol:mainfrom
bbdu3:fix/prm-resource-validation

Conversation

@bbdu3

@bbdu3 bbdu3 commented Nov 10, 2026

Copy link
Copy Markdown

Summary

The client previously validated Protected Resource Metadata (PRM) using checkResourceAllowed, which checks same-origin path containment. This allowed metadata for a child endpoint to claim a parent resource, and that broader resource was then sent in both authorization and token requests. The comparison also ignored query parameters.

RFC 9728 §3.3 requires the returned resource to match the identifier associated with the discovery request. The comparison target depends on how discovery occurred:

  • Challenge discovery: the URL used to request the protected resource.
  • Path-specific well-known discovery: the resource identifier used to construct that discovery URL.
  • Root well-known fallback: the root resource identifier.

This change validates PRM identity before its authorization-server information is used, preserves legitimate root fallback, and applies the same validation to cached metadata.

Fixes #3077.

Changed

Validate identity before using PRM

  • Replace path-containment validation in the client’s PRM flow with resource-identity validation.
  • Validate fetched metadata before reading authorization_servers, discovering the authorization server, or persisting discovery state.
  • Propagate resource mismatches instead of treating them as “PRM unavailable” and continuing through the legacy fallback.
  • Apply identity validation to direct selectResourceURL calls.

Preserve discovery semantics

  • Distinguish successful root fallback from challenge and path-specific discovery.
  • Require root-fallback metadata to identify the root resource.
  • Preserve trailing path slashes when constructing PRM well-known URLs so the discovery URL remains consistent with the expected identifier.
  • Preserve path spelling, query parameters, and fragments during comparison; normalize authority representation and the empty root path.

Validate cached metadata

  • Add the optional resourceMetadataRootFallback discovery-state marker.
  • Revalidate cached metadata before using it.
  • Rediscover legacy root-resource caches that lack sufficient discovery provenance.
  • Prevent a cached root-fallback marker from relaxing validation for an explicit challenge.
  • Preserve the existing authorization-server binding checks during callback handling.

Preserve valid OAuth behavior

Compatibility: Previously accepted parent-path resources, mismatched queries, and differing trailing path slashes are now rejected. Providing validateResourceURL no longer bypasses PRM identity validation. These changes are documented in the OAuth guide and a client patch changeset.

Test

Regression coverage

Added 26 regression tests covering:

  • Parent and root resource claims through challenge discovery.
  • Parent resource claims through path-specific discovery.
  • Valid root fallback and invalid non-root claims from root metadata.
  • Query, origin, sibling-path, path-segment, trailing-slash, dot-segment, encoded-path, and fragment mismatches.
  • Rejection before authorization-server discovery, provider hooks, or cache persistence.
  • Authorization, cached code exchange, and refresh with unchanged resource parameters.
  • Invalid cached metadata, partial caches, legacy root-cache rediscovery, and explicit challenges against cached root metadata.
  • Direct resource selection and custom selection after successful validation.

Running these tests against the original implementation produced 22 failures and 4 passes. All 26 pass with the fix.

Existing regression suites

  • Client package: 45 test files, 1104 tests passed.
  • Shared authentication utilities: 12 tests passed.
  • Updated existing OAuth and redirect fixtures to publish metadata matching the resource being discovered; retained the original redirect assertions.
  • git diff --check passed.

Validated by Node.js 24.18.0 and pnpm 10.26.1.

@bbdu3
bbdu3 requested a review from a team as a code owner November 10, 2026 01:36
@changeset-bot

changeset-bot Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 96a7000

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

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/server Patch
@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 Nov 10, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

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

@modelcontextprotocol/core

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

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 96a7000

@SameRainbows

Copy link
Copy Markdown

Enforcing RFC 9728 §3.3 resource identity validation directly closes the path containment gap in PRM discovery (#3077).

Replacing checkResourceAllowed path containment with exact identifier matching ensures that metadata returned for a sub-resource cannot claim a broader parent resource or drop query parameters during authorization and token exchange.

A few key points verified in the implementation:

  1. Distinguishing challenge discovery, path-specific discovery, and root-fallback discovery targets the expected identifier without breaking legitimate root metadata fallbacks.
  2. The resourceMetadataRootFallback state marker ensures cached metadata is revalidated against its discovery provenance before reuse, preventing stale root entries from relaxing validation on explicit challenges.
  3. Preserving the exact resource identifier string across authorization, code exchange, and token refresh keeps interoperability intact for servers expecting the original request parameter.
  4. All 14 CI matrix jobs (Node 20, 22, 24, Bun, Deno, conformance) are green, and all 45 test files pass cleanly.

@PhysicallyActive

Copy link
Copy Markdown

Thank you for this PR. The change fixes the problem in my issue ( #3077 ). I am not a TypeScript developer, so I did not examine all of the code.

Best notify a CODEOWNER that this change can break existing clients when merged. For example, discoverOAuthProtectedResourceMetadata is public, and now it can throw a mismatch error. The changeset identifies a patch release. Please examine if a minor release is better.

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Nov 10, 2026

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

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.

[v2] Client accepts a parent-path protected resource metadata, is this intended given RFC 9728?

3 participants