Skip to content

fix(auth): preserve resource URI without trailing slash (#1968) - #1972

Merged
KKonstantinov merged 6 commits into
modelcontextprotocol:v1.xfrom
MukundaKatta:fix/oauth-resource-trailing-slash-v1x
Sep 3, 2026
Merged

fix(auth): preserve resource URI without trailing slash (#1968)#1972
KKonstantinov merged 6 commits into
modelcontextprotocol:v1.xfrom
MukundaKatta:fix/oauth-resource-trailing-slash-v1x

Conversation

@MukundaKatta

@MukundaKatta MukundaKatta commented Apr 28, 2026

Copy link
Copy Markdown

What

Fixes #1968.

When parsing OAuth protected resource metadata, selectResourceURL returned new URL(resourceMetadata.resource) and the auth + token request paths serialized that value via URL.href. For bare-origin URIs that round trip appends a trailing slash:

new URL("https://example.com").href // "https://example.com/"

So a PRM document that publishes "resource": "https://example.com" ends up sending resource=https%3A%2F%2Fexample.com%2F on the wire. That changes the resource indicator from the value the server told us to use, and it breaks any AS that compares it against the configured audience exactly.

In particular Microsoft Entra ID rejects the call with:

AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes.

(see also: modelcontextprotocol/inspector#927 for the symptom downstream.)

How

  • selectResourceURL now returns URL | string | undefined and returns resourceMetadata.resource verbatim from the PRM path. The custom-validation path still returns a URL because validateResourceURL is typed that way.
  • startAuthorization, executeTokenRequest, exchangeAuthorization, refreshAuthorization, and fetchToken accept resource?: URL | string and serialize it via String(resource) instead of resource.href.
  • The validateResourceURL provider hook signature is unchanged. checkResourceAllowed still validates the metadata value as a URL.

Diff is contained to src/client/auth.ts. Existing PRM-with-path coverage still passes (paths like https://api.example.com/mcp-server are unaffected — the URL.href round-trip only added a slash for bare origins).

Tests

  • Added a regression test that publishes resource: "https://example.com" in PRM and asserts the auth URL receives resource=https://example.com (no trailing slash).
  • Updated the cached-discovery-state test, which previously asserted the buggy normalized form, to expect the un-normalized resource string.
  • npm test passes locally (1580/1580).

Maintainer edit:

Backport of the #1968 fix to v1.x, aligned with the v2 change in #2581: selectResourceURL keeps its signature, auth() sends the protected resource metadata's resource string verbatim, and the OAuth helpers accept string | URL.

…protocol#1968)

When handling RFC 9728 protected resource metadata, `selectResourceURL`
routed the metadata's `resource` value through `new URL(...).href`. For
bare-origin URIs that round trip appends a trailing slash:

    new URL("https://example.com").href === "https://example.com/"

The resulting `resource` parameter no longer matches what the server
published in PRM, which breaks providers that require an exact match.
Microsoft Entra ID rejects the request with AADSTS9010010 when the
`resource` parameter does not match the audience of the requested scope.

Return the original metadata string verbatim from `selectResourceURL`
and serialize it with `String(resource)` instead of `URL.href` in the
authorization and token request paths. The validation step still parses
the value as a URL via `checkResourceAllowed`. Also adjusted the cached
discovery-state test to expect the un-normalized resource value, and
added a regression test for the bare-domain case.

Fixes modelcontextprotocol#1968
@MukundaKatta
MukundaKatta requested review from a team as code owners April 28, 2026 03:49
@changeset-bot

changeset-bot Bot commented Apr 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1f14cb2

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

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 Apr 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@1972

commit: 1f14cb2

MukundaKatta and others added 5 commits April 28, 2026 08:21
…resource string from auth() (modelcontextprotocol#1968)

Align the v1.x fix with the v2 shape (modelcontextprotocol#2581): the public signature of
selectResourceURL stays Promise<URL | undefined>; auth() sends the protected
resource metadata's `resource` string verbatim when the provider has no custom
validateResourceURL, and the OAuth helpers accept `string | URL` for `resource`
via a small resourceIndicatorToString serializer.

- auth.test.ts: the regression test now drives both the authorization redirect
  and the authorization-code token exchange with PRM `resource: https://example.com`
  and asserts neither gains a trailing slash; add a startAuthorization string case
- auth.ts: JSDoc on selectResourceURL and a comment at the decision site in auth()
- changeset: describe the normalization, the Entra rejection, the widened inputs,
  and that selectResourceURL's signature is unchanged
@KKonstantinov KKonstantinov self-assigned this Sep 3, 2026
@KKonstantinov KKonstantinov added the v1 Issues / PRs related to v1.x label Sep 3, 2026
@KKonstantinov
KKonstantinov merged commit 12b4256 into modelcontextprotocol:v1.x Sep 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v1 Issues / PRs related to v1.x

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants