You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implements the OAuth mix-up defenses, default-ON.
- IssuerMismatchError {kind:'metadata'|'authorization_response'} in
authErrors.ts (extends OAuthClientFlowError, not OAuthError, so the
auth() retry block does not swallow it). Re-exported from auth.ts for
back-compat.
- New validateAuthorizationResponseIssuer() pure helper implementing the
spec's 4-row decision table with exact === comparison (no normalization).
- discoverAuthorizationServerMetadata() validates issuer against the raw
discovery input per RFC 8414 §3.3; throws IssuerMismatchError{kind:metadata}
on mismatch unless skipIssuerValidation. One-directional trailing-slash
tolerance for the SDK-synthesized legacy-fallback URL only.
- authInternal/exchangeAuthorization/fetchToken validate iss before token
exchange (gated on authorizationCode \!== undefined).
- finishAuth(URLSearchParams) overload on StreamableHTTPClientTransport and
SSEClientTransport (reads code + iss); finishAuth(code, iss?) kept for
back-compat.
- AuthorizationServerMetadata.authorization_response_iss_parameter_supported
schema field (z.boolean().optional().catch(undefined) — tolerant of
non-boolean wire values).
- skipIssuerMetadataValidation suppresses AU-02 (metadata echo) only, not the
AU-01 runtime iss check.
- IssuerMismatchError.message JSON-encodes received/expected (log-injection
guard for attacker-controllable iss).
Conformance: auth/metadata-issuer-mismatch burns. The 6 auth/iss-* cells'
SEP-2468 check passes — they stay listed because the referee bundles a
SEP-837 application_type DCR check (burns with PR-B).
auth/2025-03-26-oauth-metadata-backcompat is added to the baseline: the §3.3
issuer-echo check correctly rejects the 0.2.0-alpha.5 referee's backcompat
mock (issuer carries a path component); tracked for a referee-side fix at the
next pin bump.
E2E: client-auth:iss:{match,mismatch-reject,supported-missing-reject,
unadvertised-proceed,no-normalize,opt-out}; as-metadata-discovery:issuer-
validation flips from knownFailure to pass.
Existing-test fixtures aligned to RFC 8414 §3.3 (issuer values only; the tests
assert other behavior).
Claude-Session: https://claude.ai/code/session_01XBib5gRe8AMPPJhySCz3EJ
Implement RFC 9207 / RFC 8414 §3.3 OAuth issuer validation (SEP-2468). `discoverAuthorizationServerMetadata()` now rejects metadata whose `issuer` does not match the discovery URL (opt out via `skipIssuerValidation` / `AuthOptions.skipIssuerMetadataValidation` — security-weakening). `auth()`, `exchangeAuthorization()`, `fetchToken()`, and `transport.finishAuth(code, iss?)` now validate the authorization-callback `iss` against the recorded issuer before redeeming the code; new `IssuerMismatchError` and `validateAuthorizationResponseIssuer()` are exported.
Copy file name to clipboardExpand all lines: docs/client.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -192,8 +192,8 @@ Server only implements `client_secret_basic`/`client_secret_post`, so there is n
192
192
### Full OAuth with user authorization
193
193
194
194
For user-facing applications, implement the {@linkcode@modelcontextprotocol/client!client/auth.OAuthClientProvider | OAuthClientProvider} interface to handle the full authorization code flow (redirects, code verifiers, token storage, dynamic client registration). The {@linkcode
195
-
@modelcontextprotocol/client!client/client.Client#connect | connect()} call will throw {@linkcode@modelcontextprotocol/client!client/auth.UnauthorizedError | UnauthorizedError} when authorization is needed — catch it, complete the browser flow, call {@linkcode
196
-
@modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport#finishAuth | transport.finishAuth(code)}, and reconnect.
195
+
@modelcontextprotocol/client!client/client.Client#connect | connect()} call will throw {@linkcode@modelcontextprotocol/client!client/auth.UnauthorizedError | UnauthorizedError} when authorization is needed — catch it, complete the browser flow, pass the redirect URL's query to {@linkcode
196
+
@modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport#finishAuth | transport.finishAuth(url.searchParams)} (so the SDK can validate the RFC 9207 `iss` parameter), and reconnect.
197
197
198
198
For a complete working OAuth flow, see [`simpleOAuthClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/oauth/simpleOAuthClient.ts) and
Copy file name to clipboardExpand all lines: docs/migration-SKILL.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -576,6 +576,10 @@ side: auto-fulfilment is on by default (`ClientOptions.inputRequired`, `maxRound
576
576
577
577
Output-schema validator compilation is now lazy (first `callTool()` against the cached `tools/list` entry) and non-throwing (an uncompilable `outputSchema` is `console.warn`-ed and validation is skipped for that tool only); `listTools()` no longer throws on an uncompilable `outputSchema`. Applies on every era — the legacy-era `listTools()` path is unchanged at the wire level only.
578
578
579
+
OAuth callback handling: pass the callback URL's `URLSearchParams` to `transport.finishAuth(url.searchParams)` (or pass `iss` alongside `authorizationCode` to `auth()` / `finishAuth(code, iss)`). The SDK now validates `iss` per RFC 9207: a mismatched `iss` throws `IssuerMismatchError` regardless of advertised support; a missing `iss` throws only when the AS advertised `authorization_response_iss_parameter_supported: true`. Do not surface `error_description` / `error_uri` from a callback that failed this check.
580
+
581
+
`discoverAuthorizationServerMetadata()` now rejects metadata whose `issuer` does not exactly match the URL it was fetched for (RFC 8414 §3.3), throwing `IssuerMismatchError`. Pass `skipIssuerMetadataValidation: true` on `AuthOptions` (or `skipIssuerValidation: true` on the helper) only as a temporary workaround for a known-misconfigured AS.
582
+
579
583
No code changes required; wire-behavior note: on a 2026-07-28 Streamable HTTP connection, aborting an in-flight client request (caller `signal` / timeout) closes that request's SSE response stream as the spec cancellation signal — `notifications/cancelled` is no longer POSTed
580
584
there. 2025-era connections and stdio at any era still send `notifications/cancelled`. Custom `Transport` implementations that open one underlying request per outbound message and honor `TransportSendOptions.requestSignal` may declare `readonly hasPerRequestStream = true` to opt
Copy file name to clipboardExpand all lines: docs/migration.md
+20-1Lines changed: 20 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1491,9 +1491,28 @@ The inline options object on `auth()` is now the named `AuthOptions` type, expor
1491
1491
1492
1492
`OAuthTokens` and `OAuthClientInformation` also gain an optional `issuer?: string` field. Once the SEP-2352 behavior change lands the SDK will stamp this onto credentials before calling `saveTokens` / `saveClientInformation`; provider implementations should round-trip it unchanged. The field is currently inert.
**Action required for hosts handling OAuth callbacks.**
1497
+
1498
+
`transport.finishAuth()` and `auth()` now validate the `iss` parameter from the authorization callback against the issuer recorded from the authorization server's validated metadata (RFC 9207). A **mismatched**`iss` is rejected with `IssuerMismatchError` before the code is exchanged regardless of what the AS advertised; a **missing**`iss` is rejected only when the AS advertised `authorization_response_iss_parameter_supported: true`.
1499
+
1500
+
**You must** pass the callback URL's query parameters to the SDK so it can read `iss` alongside `code`:
`transport.finishAuth(code, iss)` remains supported for back-compat. If you bypass `auth()` and call `exchangeAuthorization()` / `fetchToken()` directly, pass `iss` in the options bag — the same validation runs there.
1508
+
1509
+
**You must not** display or act on `error`, `error_description`, or `error_uri` from the callback URL when `IssuerMismatchError` is thrown — those values are attacker-controlled in a mix-up attack.
1510
+
1511
+
`discoverAuthorizationServerMetadata()` now rejects metadata whose `issuer` does not exactly match the URL it was fetched for (RFC 8414 §3.3). If you connect to a known-misconfigured AS, set `skipIssuerMetadataValidation: true` on `AuthOptions` (or `skipIssuerValidation: true` on the low-level helper) — **this weakens the mix-up defense and should be treated as a temporary workaround.** It suppresses only the metadata-echo check; the callback-`iss` validation always runs (and degrades to a no-op only when `iss` is absent and the AS does not advertise support).
1512
+
1494
1513
### Conformance obligations for `OAuthClientProvider` implementers
1495
1514
1496
-
<!-- Filled in as the SEP-2468/2352/2350/837/2207 behavior PRs land. -->
1515
+
<!-- Filled in as the SEP-2352/2350/837/2207 behavior PRs land. -->
Copy file name to clipboardExpand all lines: examples/oauth/README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,7 +5,7 @@ The **authorization-code** OAuth grant — the interactive "user signs in and ap
5
5
-`server.ts` — `setupAuthServer` (the better-auth/OIDC demo Authorization Server from `@mcp-examples/shared`) on `:PORT+1`, and a `createMcpHandler` Resource Server behind `requireBearerAuth({ verifier: demoTokenVerifier })` on `:PORT/mcp`, advertising the AS via
6
6
`createProtectedResourceMetadataRouter` (RFC 9728). DEMO ONLY — the AS auto-signs-in a fixed user, and with `OAUTH_DEMO_AUTO_CONSENT=1` it also auto-approves the consent screen.
7
7
-`client.ts` — **CI-runnable headless flow.** Drives the same SDK auth machinery as the browser client, but instead of `open()`ing the authorization URL it follows the 302 chain itself with `fetch(..., { redirect: 'manual' })` (the demo AS's auto-sign-in + auto-consent collapse
8
-
every interactive step into a redirect), reads the `code`off the final `Location` header, calls `transport.finishAuth(code)`, reconnects, and asserts `ctx.authInfo` round-trips. This is what the harness runs.
8
+
every interactive step into a redirect), reads the callback query off the final `Location` header, calls `transport.finishAuth(url.searchParams)` (so the SDK reads `code` + `iss` per RFC 9207), reconnects, and asserts `ctx.authInfo` round-trips. This is what the harness runs.
9
9
-`simpleOAuthClient.ts` + `simpleOAuthClientProvider.ts` — **manual real-browser flow.** Full authorization-code flow against any OAuth-protected MCP server: opens the browser, runs a local callback server on `:8090`, exchanges the code, then drops into a small `list`/`call`
10
10
REPL. Run this when you want to see the consent page.
11
11
-`dualModeAuth.ts` — two auth patterns through the one `authProvider` option: host-managed bearer token vs a built-in `OAuthClientProvider`.
0 commit comments