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
{{ message }}
Repository navigation
Commit 646cea6
Browse filesBrowse the repository at this point in the historyBrowse files
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
@@ -1528,16 +1528,19 @@ The inline options object on `auth()` is now the named `AuthOptions` type, expor
1528
1528
1529
1529
`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`.
1530
1530
1531
-
**You must** pass the callback URL's query parameters to the SDK so it can read `iss` alongside `code`:
1531
+
**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.
1539
1540
1540
-
**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.
1541
+
**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.
1542
+
1543
+
_(`@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.
1541
1544
1542
1545
`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).
@@ -229,10 +238,15 @@ export class SSEClientTransport implements Transport {
229
238
/**
230
239
* 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.
231
240
*
232
-
* Prefer passing the callback URL's `searchParams` directly — the SDK extracts
233
-
* `code` and `iss` (and validates `iss` per RFC 9207) for you. The `(code, iss?)`
241
+
* **Preferred:** pass the callback URL's `searchParams` directly. The SDK extracts `code`
242
+
* and `iss`, validates `iss` against the recorded issuer (RFC 9207) **before** reading any
243
+
* other parameter, and on mismatch throws an {@linkcode IssuerMismatchError} that carries
244
+
* none of the callback's `error`/`error_description`/`error_uri` text. The `(code, iss?)`
234
245
* positional form remains supported for back-compat.
235
246
*
247
+
* The SDK does **not** validate `state`; compare it to your stored value before calling
248
+
* `finishAuth`.
249
+
*
236
250
* @param callbackParams - The `URLSearchParams` from the authorization callback URL
237
251
* (e.g. `new URL(callbackUrl).searchParams`). `code` and `iss` are read from it.
238
252
*/
@@ -248,22 +262,18 @@ export class SSEClientTransport implements Transport {
248
262
thrownewUnauthorizedError('finishAuth requires an OAuthClientProvider');
249
263
}
250
264
251
-
letauthorizationCode: string;
252
-
if(codeOrParamsinstanceofURLSearchParams){
253
-
constcode=codeOrParams.get('code');
254
-
if(!code){
255
-
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
@@ -816,10 +817,16 @@ export class StreamableHTTPClientTransport implements Transport {
816
817
/**
817
818
* 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.
818
819
*
819
-
* Prefer passing the callback URL's `searchParams` directly — the SDK extracts
820
-
* `code` and `iss` (and validates `iss` per RFC 9207) for you. The `(code, iss?)`
820
+
* **Preferred:** pass the callback URL's `searchParams` directly. The SDK extracts `code`
821
+
* and `iss`, validates `iss` against the recorded issuer (RFC 9207) **before** reading any
822
+
* other parameter, and on mismatch throws an {@linkcode IssuerMismatchError} that carries
823
+
* none of the callback's `error`/`error_description`/`error_uri` text — those are
824
+
* attacker-controlled in a mix-up attack and MUST NOT be displayed. The `(code, iss?)`
821
825
* positional form remains supported for back-compat.
822
826
*
827
+
* The SDK does **not** validate `state`; compare it to your stored value before calling
828
+
* `finishAuth`.
829
+
*
823
830
* @param callbackParams - The `URLSearchParams` from the authorization callback URL
824
831
* (e.g. `new URL(callbackUrl).searchParams`). `code` and `iss` are read from it.
825
832
*/
@@ -837,22 +844,18 @@ export class StreamableHTTPClientTransport implements Transport {
837
844
thrownewUnauthorizedError('finishAuth requires an OAuthClientProvider');
838
845
}
839
846
840
-
letauthorizationCode: string;
841
-
if(codeOrParamsinstanceofURLSearchParams){
842
-
constcode=codeOrParams.get('code');
843
-
if(!code){
844
-
thrownewUnauthorizedError('Authorization callback is missing the "code" parameter');
0 commit comments