Skip to content

fix(client): preserve resource_metadata URL across non-Bearer WWW-Authenticate challenges#1951

Open
Zelys-DFKH wants to merge 1 commit intomodelcontextprotocol:mainfrom
Zelys-DFKH:fix/oauth-non-bearer-www-authenticate
Open

fix(client): preserve resource_metadata URL across non-Bearer WWW-Authenticate challenges#1951
Zelys-DFKH wants to merge 1 commit intomodelcontextprotocol:mainfrom
Zelys-DFKH:fix/oauth-non-bearer-www-authenticate

Conversation

@Zelys-DFKH
Copy link
Copy Markdown

Summary

`extractWWWAuthenticateParams` returns `{}` — and therefore `undefined` for
`resourceMetadataUrl` — when `WWW-Authenticate` is a non-Bearer scheme like
`Negotiate`. That's correct. But both transports then unconditionally wrote that
`undefined` back to `_resourceMetadataUrl`, wiping out any URL stored from an
earlier Bearer challenge.

`discoverMetadataWithFallback` uses that field as its starting point. `undefined`
triggers the right fallback: construct the well-known URI from `serverUrl`. But if
a real Bearer URL was stored first and a later `Negotiate` 401 cleared it, discovery
restarts from the wrong origin. The MCP auth spec requires clients to fall back to
well-known when `WWW-Authenticate` is absent or does not carry a Bearer
`resource_metadata` parameter; clearing a previously-stored URL silently breaks that
for multi-challenge sequences.

`UnauthorizedContext` gets an optional `resourceMetadataUrl` field so transports can
pass the stored Bearer URL into the callback. `handleOAuthUnauthorized` now uses
`extractedUrl ?? ctx.resourceMetadataUrl`, falling back to the stored URL when the
current challenge has none. Both `StreamableHTTPClientTransport` and `SSEClientTransport`
now guard `_resourceMetadataUrl` and `_scope` updates to only overwrite when the
extracted value is non-`undefined`, and include `this._resourceMetadataUrl` in every
`onUnauthorized` context.

Test plan

  • `pnpm --filter @modelcontextprotocol/client test`: 363 tests pass (55 in
    `streamableHttp.test.ts`, up from 53)
  • `pnpm --filter @modelcontextprotocol/client typecheck`: clean
  • New in `test/client/streamableHttp.test.ts`: one test verifies `redirectToAuthorization`
    fires on a `Negotiate` 401 with no `resource_metadata`; a second verifies a `Negotiate`
    401 following a Bearer 401 does not clear `_resourceMetadataUrl` and the stored URL
    reaches `onUnauthorized` via context.

Fixes #1946

…henticate challenges

When WWW-Authenticate carries a non-Bearer scheme (e.g. Negotiate),
extractWWWAuthenticateParams returns undefined for resourceMetadataUrl.
Both transports were unconditionally writing that undefined back to
_resourceMetadataUrl, wiping out any URL stored from an earlier Bearer
challenge and breaking PRM discovery for multi-challenge sequences.

- Add optional resourceMetadataUrl to UnauthorizedContext so transports
  can pass the stored Bearer URL into the onUnauthorized callback
- handleOAuthUnauthorized now uses extractedUrl ?? ctx.resourceMetadataUrl,
  falling back to the stored URL when the current challenge has none
- Both StreamableHTTPClientTransport and SSEClientTransport guard
  _resourceMetadataUrl and _scope updates to only overwrite when the
  extracted value is non-undefined, and include this._resourceMetadataUrl
  in every onUnauthorized context

Fixes modelcontextprotocol#1946

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Zelys-DFKH Zelys-DFKH requested a review from a team as a code owner April 23, 2026 17:30
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 23, 2026

🦋 Changeset detected

Latest commit: 3295106

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

This PR includes changesets to release 1 package
Name Type
@modelcontextprotocol/client 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
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/server

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

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 3295106

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.

OAuth discovery does not fall back to well-known URI when 401 carries a non-Bearer WWW-Authenticate (e.g. Negotiate)

1 participant