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
SEP-2468 follow-up: `transport.finishAuth()` gains a `URLSearchParams` overload (preferred) that extracts `code`/`iss`, validates `iss` first, and on mismatch throws a sanitized `IssuerMismatchError` (no callback `error_description` text); callers remain responsible for `state`. **Behavior change for `@modelcontextprotocol/server-legacy`:**`mcpAuthRouter` now advertises `authorization_response_iss_parameter_supported` (default `true`; `ProxyOAuthServerProvider` reports `false`) and the bundled authorize handler appends `iss` (RFC 9207) to every `res.redirect(...)` your `OAuthServerProvider.authorize()` issues to the client's `redirect_uri`. If your provider redirects another way (`res.writeHead`, a separate consent-page response, or a standalone `authorizationHandler({provider})` without `issuerUrl`), append `params.issuer` as `iss` yourself or set `authorizationResponseIssParameterSupported: false` — otherwise RFC 9207-compliant clients (including this SDK) will reject the callback.
Copy file name to clipboardExpand all lines: docs/migration.md
+7-4Lines changed: 7 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1531,16 +1531,19 @@ New TypeScript-only aliases `StoredOAuthTokens` and `StoredOAuthClientInformatio
1531
1531
1532
1532
`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`.
1533
1533
1534
-
**You must** pass the callback URL's query parameters to the SDK so it can read `iss` alongside `code`:
1534
+
**You must** pass the callback URL's query parameters to the SDK so it can read `iss` alongside `code`. The SDK does **not** validate `state`; compare it to your stored value before calling `finishAuth`:
`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.
1542
1543
1543
-
**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.
1544
+
**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. The `URLSearchParams` overload handles this for you; if you parse the callback yourself, suppress them.
1545
+
1546
+
_(`@modelcontextprotocol/server-legacy` AS implementers — **behavior change**)_ `mcpAuthRouter()` now advertises `authorization_response_iss_parameter_supported` (default `true`) and the bundled authorize handler appends `iss` to **every** redirect — success or error — that your `OAuthServerProvider.authorize()` issues to the client's `redirect_uri` **via `res.redirect(...)` on the supplied `res`**. No provider change is required when that is how you redirect. If you emit the `Location` header another way (e.g. `res.writeHead(302, { Location })`), issue the final callback redirect from a different response (e.g. after a separate consent-page POST), or wire a standalone `authorizationHandler({provider})` without `issuerUrl`, append `params.issuer` as `iss` yourself — otherwise RFC 9207-compliant clients (including this SDK's) will reject the callback with `IssuerMismatchError`. If the callback is issued by an upstream AS you proxy to, set `authorizationResponseIssParameterSupported = false` on your provider (`ProxyOAuthServerProvider` does this) so the metadata does not over-claim.
1544
1547
1545
1548
`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 `StreamableHTTPClientTransportOptions` / `SSEClientTransportOptions` (or on `AuthOptions` if you call `auth()` directly, 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).
@@ -242,10 +251,15 @@ export class SSEClientTransport implements Transport {
242
251
/**
243
252
* Call this method after the user has finished authorizing via their user agent and is redirected back to the MCP client application. This will exchange the authorization code for an access token, enabling the next connection attempt to successfully auth.
244
253
*
245
-
* Prefer passing the callback URL's `searchParams` directly — the SDK extracts
246
-
* `code` and `iss` (and validates `iss` per RFC 9207) for you. The `(code, iss?)`
254
+
* **Preferred:** pass the callback URL's `searchParams` directly. The SDK extracts `code`
255
+
* and `iss`, validates `iss` against the recorded issuer (RFC 9207) **before** reading any
256
+
* other parameter, and on mismatch throws an {@linkcode IssuerMismatchError} that carries
257
+
* none of the callback's `error`/`error_description`/`error_uri` text. The `(code, iss?)`
247
258
* positional form remains supported for back-compat.
248
259
*
260
+
* The SDK does **not** validate `state`; compare it to your stored value before calling
261
+
* `finishAuth`.
262
+
*
249
263
* @param callbackParams - The `URLSearchParams` from the authorization callback URL
250
264
* (e.g. `new URL(callbackUrl).searchParams`). `code` and `iss` are read from it.
251
265
*/
@@ -261,22 +275,18 @@ export class SSEClientTransport implements Transport {
261
275
thrownewUnauthorizedError('finishAuth requires an OAuthClientProvider');
262
276
}
263
277
264
-
letauthorizationCode: string;
265
-
if(codeOrParamsinstanceofURLSearchParams){
266
-
constcode=codeOrParams.get('code');
267
-
if(!code){
268
-
thrownewUnauthorizedError('Authorization callback is missing the "code" parameter');
Copy file name to clipboardExpand all lines: packages/client/src/client/streamableHttp.ts
+17-14Lines changed: 17 additions & 14 deletions
Original file line number
Diff line number
Diff line change
@@ -26,6 +26,7 @@ import {
26
26
extractWWWAuthenticateParams,
27
27
isOAuthClientProvider,
28
28
isStrictScopeSuperset,
29
+
resolveAuthorizationCallbackParams,
29
30
UnauthorizedError
30
31
}from'./auth.js';
31
32
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- referenced via {@linkcode} in finishAuth JSDoc
@@ -829,10 +830,16 @@ export class StreamableHTTPClientTransport implements Transport {
829
830
/**
830
831
* Call this method after the user has finished authorizing via their user agent and is redirected back to the MCP client application. This will exchange the authorization code for an access token, enabling the next connection attempt to successfully auth.
831
832
*
832
-
* Prefer passing the callback URL's `searchParams` directly — the SDK extracts
833
-
* `code` and `iss` (and validates `iss` per RFC 9207) for you. The `(code, iss?)`
833
+
* **Preferred:** pass the callback URL's `searchParams` directly. The SDK extracts `code`
834
+
* and `iss`, validates `iss` against the recorded issuer (RFC 9207) **before** reading any
835
+
* other parameter, and on mismatch throws an {@linkcode IssuerMismatchError} that carries
836
+
* none of the callback's `error`/`error_description`/`error_uri` text — those are
837
+
* attacker-controlled in a mix-up attack and MUST NOT be displayed. The `(code, iss?)`
834
838
* positional form remains supported for back-compat.
835
839
*
840
+
* The SDK does **not** validate `state`; compare it to your stored value before calling
841
+
* `finishAuth`.
842
+
*
836
843
* @param callbackParams - The `URLSearchParams` from the authorization callback URL
837
844
* (e.g. `new URL(callbackUrl).searchParams`). `code` and `iss` are read from it.
838
845
*/
@@ -850,22 +857,18 @@ export class StreamableHTTPClientTransport implements Transport {
850
857
thrownewUnauthorizedError('finishAuth requires an OAuthClientProvider');
851
858
}
852
859
853
-
letauthorizationCode: string;
854
-
if(codeOrParamsinstanceofURLSearchParams){
855
-
constcode=codeOrParams.get('code');
856
-
if(!code){
857
-
thrownewUnauthorizedError('Authorization callback is missing the "code" parameter');
0 commit comments