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
@@ -191,9 +201,114 @@ Server only implements `client_secret_basic`/`client_secret_post`, so there is n
191
201
192
202
### Full OAuth with user authorization
193
203
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, 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.
204
+
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). Key persisted
205
+
client credentials by the `ctx.issuer` passed to `clientInformation()` / `saveClientInformation()` so credentials registered with one authorization server are never sent to another:
// In production, persist to OS keychain / secure storage — never plain files.
237
+
this.storedTokens=tokens;
238
+
}
239
+
// CSRF binding for the redirect — the SDK puts this on the authorize URL;
240
+
// your callback handler compares it before calling `finishAuth`.
241
+
state() {
242
+
this.lastState=crypto.randomUUID();
243
+
returnthis.lastState;
244
+
}
245
+
// Callback-leg AS-binding (SEP-2352): record what discovery resolved before
246
+
// the redirect so the SDK can verify the code is exchanged at the same AS.
247
+
saveDiscoveryState(state:OAuthDiscoveryState) {
248
+
this.discovery=state;
249
+
}
250
+
discoveryState() {
251
+
returnthis.discovery;
252
+
}
253
+
redirectToAuthorization(url:URL) {
254
+
onRedirect(url);
255
+
}
256
+
saveCodeVerifier(v:string) {
257
+
this.verifier=v;
258
+
}
259
+
codeVerifier() {
260
+
if (!this.verifier) thrownewError('no code verifier');
261
+
returnthis.verifier;
262
+
}
263
+
}
264
+
265
+
const provider =newMyOAuthProvider();
266
+
const transport =newStreamableHTTPClientTransport(newURL('http://localhost:3000/mcp'), {
267
+
authProvider: provider
268
+
});
269
+
```
270
+
271
+
The {@linkcode@modelcontextprotocol/client!client/client.Client#connect | connect()} call throws {@linkcode@modelcontextprotocol/client!client/auth.UnauthorizedError | UnauthorizedError} when authorization is needed — catch it, complete the browser flow, hand the callback query
272
+
to {@linkcode@modelcontextprotocol/client!client/streamableHttp.StreamableHTTPClientTransport#finishAuth | transport.finishAuth()}, and reconnect. Passing the whole `URLSearchParams` lets the SDK extract `code` and validate the RFC 9207 `iss` parameter for you:
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.md
+19-10Lines changed: 19 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1580,20 +1580,21 @@ client logic; key off the HTTP `404` status instead.
1580
1580
1581
1581
## Authorization (2026-07-28 spec)
1582
1582
1583
-
The 2026-07-28 protocol revision adds client-side authorization requirements (RFC 9207 `iss` validation, RFC 8414 §3.3 issuer-echo, per-authorization-server credential isolation, scope step-up, DCR `application_type`, and refresh-token guidance). The SDK adds the public surface for these now and will implement the parts that land in SDK code (defaulting them on) as the SEP-2468/2352/2350/837/2207 behavior changes land; the parts that live in your `OAuthClientProvider` implementation, your `clientMetadata`, or your host UI are listed under [Conformance obligations for `OAuthClientProvider` implementers](#conformance-obligations-for-oauthclientprovider-implementers).
1583
+
The 2026-07-28 protocol revision adds client-side authorization requirements (RFC 9207 `iss` validation, RFC 8414 §3.3 issuer-echo, per-authorization-server credential isolation, scope step-up, DCR `application_type`, and refresh-token guidance). The SDK implements the parts that land in SDK code and defaults them on; the parts that live in your `OAuthClientProvider` implementation, your `clientMetadata`, or your host UI are listed under [Conformance obligations for `OAuthClientProvider` implementers](#conformance-obligations-for-oauthclientprovider-implementers).
1584
1584
1585
1585
### `auth()` options are now `AuthOptions`
1586
1586
1587
-
The inline options object on `auth()` is now the named `AuthOptions` type, exported from `@modelcontextprotocol/client`. Existing call sites need no change. New fields (both currently inert — the validation behavior they feed lands in the follow-up changes tracked by SEP-2468):
1587
+
The inline options object on `auth()` is now the named `AuthOptions` type, exported from `@modelcontextprotocol/client`. Existing call sites need no change. New fields:
1588
1588
1589
-
-`iss?: string` — the form-urldecoded `iss` query parameter from the authorization callback. Pass it alongside `authorizationCode`; it is forwarded to RFC 9207 issuer validation once that lands.
1590
-
-`skipIssuerMetadataValidation?: boolean` — opt-out for the RFC 8414 §3.3 issuer-echo check during discovery. **Security-weakening**; use only with authorization servers known to publish a mismatched `issuer`.
1589
+
-`iss?: string` — the form-urldecoded `iss` query parameter from the authorization callback. Pass it alongside `authorizationCode` so the SDK can validate it per RFC 9207 before redeeming the code.
1590
+
-`skipIssuerMetadataValidation?: boolean` — opt-out of the RFC 8414 §3.3 issuer-echo check during discovery. **Security-weakening**; use only with authorization servers known to publish a mismatched `issuer`.
1591
+
-`forceReauthorization?: boolean` — skip the refresh-token branch and force a fresh authorization request. Set by the transport's step-up path when the required scope strictly exceeds the current token's; hosts driving step-up themselves set it under the same condition. See [Scope step-up](#scope-step-up-on-403-insufficient_scope-sep-2350).
1591
1592
1592
1593
### `OAuthClientProvider` credential methods receive an `issuer` context
1593
1594
1594
-
`clientInformation(ctx?)`, `saveClientInformation(info, ctx?)`, `tokens(ctx?)`, and `saveTokens(tokens, ctx?)` now receive an optional `OAuthClientInformationContext` parameter carrying `{ issuer: string }` — the authorization server's `issuer` identifier. Providers that persist credentials should key storage by this value so that credentials registered with one authorization server are never sent to another. Providers with a single credential set may ignore the parameter; existing implementations compile unchanged. The SDK does not yet pass this argument; it begins doing so when the SEP-2352 behavior change lands.
1595
+
`clientInformation(ctx?)`, `saveClientInformation(info, ctx?)`, `tokens(ctx?)`, and `saveTokens(tokens, ctx?)` now receive an optional `OAuthClientInformationContext` parameter carrying `{ issuer: string }` — the authorization server's `issuer` identifier. Providers that persist credentials should key storage by this value so that credentials registered with one authorization server are never sent to another. Providers with a single credential set may ignore the parameter; existing implementations compile unchanged.
1595
1596
1596
-
New TypeScript-only aliases `StoredOAuthTokens` and `StoredOAuthClientInformation` add an optional `issuer?: string` field on top of the wire types and are used as the parameter/return types of `tokens()` / `saveTokens()` and `clientInformation()` / `saveClientInformation()`. The `issuer` field is **not** part of the RFC 6749/7591 wire responses and is intentionally absent from `OAuthTokensSchema` / `OAuthClientInformationSchema` so an authorization server cannot populate it; once the SEP-2352 behavior change lands the SDK will stamp it onto credentials before calling `saveTokens` / `saveClientInformation`. Provider implementations should round-trip it unchanged. The field is currently inert.
1597
+
New TypeScript-only aliases `StoredOAuthTokens` and `StoredOAuthClientInformation` add an optional `issuer?: string` field on top of the wire types and are used as the parameter/return types of `tokens()` / `saveTokens()` and `clientInformation()` / `saveClientInformation()`. The `issuer` field is **not** part of the RFC 6749/7591 wire responses and is intentionally absent from `OAuthTokensSchema` / `OAuthClientInformationSchema` so an authorization server cannot populate it; the SDK stamps it onto credentials before calling `saveTokens` / `saveClientInformation`. Provider implementations should round-trip it unchanged. See [Per-authorization-server credential isolation](#per-authorization-server-credential-isolation-sep-2352) for how the stamp is used.
@@ -1647,13 +1648,21 @@ The bundled `ClientCredentialsProvider`, `PrivateKeyJwtProvider`, `StaticPrivate
1647
1648
1648
1649
### Conformance obligations for `OAuthClientProvider` implementers
1649
1650
1650
-
<!-- Filled in as the SEP-2352/2350/837/2207 behavior PRs land. -->
1651
+
The SDK enforces every 2026-07-28 authorization MUST that lands in SDK code. The obligations below live in **your**`OAuthClientProvider` implementation, your `clientMetadata`, your host UI, or your resource-server configuration — the SDK structurally cannot enforce them. Each links to the example that demonstrates the conformant pattern.
- **SEP-2352 — round-trip the `issuer` stamp on persisted credentials.** `saveTokens()` and `saveClientInformation()` receive values with an SDK-stamped `issuer` field; persist the value verbatim and return it verbatim from `tokens()` / `clientInformation()` and the binding holds — the SDK discards a stored value whose stamp names a different authorization server. If you serialise to a custom format, persist `issuer` alongside the rest. To hold credentials for several authorization servers at once, key your storage on `ctx.issuer` and return `undefined` for an issuer you have no entry for — but when `ctx === undefined` (the transport's per-request bearer read), return the most-recently-saved token set. You **SHOULD** implement `discoveryState()` / `saveDiscoveryState()` so the callback leg can verify it is exchanging the authorization code at the same AS the redirect targeted; without them the SDK `console.warn`s once per callback (RFC 9207 `iss` validation independently protects this leg when the AS emits `iss`). See [`examples/oauth/simpleOAuthClientProvider.ts`](../examples/oauth/simpleOAuthClientProvider.ts) for the reference pattern.
1653
1654
1654
-
**No code change required for the common case.**If your `saveTokens()` / `saveClientInformation()` persist the value passed to them verbatim and your `tokens()` / `clientInformation()` return it verbatim, the SDK-stamped `issuer` round-trips and the binding holds.
1655
+
-**SEP-2352 — pass `expectedIssuer` when supplying static client credentials.**Hosts that construct `ClientCredentialsProvider`, `PrivateKeyJwtProvider`, `StaticPrivateKeyJwtProvider`, or `CrossAppAccessProvider` with a constructor-supplied `client_secret` (or pre-signed assertion) **SHOULD** pass the new `expectedIssuer` option naming the authorization server those credentials were registered with. Without it, the credential is sent to whatever authorization server the protected resource advertises on first contact; with it, a mismatch fails before the credential leaves the process.
1655
1656
1656
-
If you serialise to a custom format, persist the `issuer` field alongside the rest of the value. If you key storage by `ctx.issuer`, return `undefined` for an issuer you have no entry for, and treat **`ctx === undefined` as "return the most-recently-saved token set"** — the transport's per-request `Authorization: Bearer` read (`adaptOAuthProvider().token()`) calls `tokens()` with no `ctx`.
1657
+
-**SEP-2207 — keep refresh tokens confidential in storage.** The SDK enforces in-transit confidentiality via the [`https:` token-endpoint guard](#token-endpoint-must-use-tls-sep-2207); in-storage confidentiality is your `saveTokens()` implementation. Use platform-appropriate secure storage (OS keychain, encrypted-at-rest store) — never persist `refresh_token` to plain files, `localStorage`, or logs.
1658
+
1659
+
-**SEP-2468 — extract `iss` from the callback URL and pass it to `finishAuth`.** Your callback handler must read the `iss` query parameter alongside `code` and call `transport.finishAuth(code, iss)` — or hand the whole `URLSearchParams` to the [overload](#authorization-server-mix-up-defense-rfc-9207--rfc-8414-33). The SDK validates the value but cannot extract it from a URL it never sees. When `IssuerMismatchError` is thrown, **do not** render the callback's raw `error` / `error_description` / `error_uri` in your UI — those values are attacker-controlled in a mix-up attack. See [`examples/oauth/simpleOAuthClient.ts`](../examples/oauth/simpleOAuthClient.ts) for the extraction pattern.
1660
+
1661
+
-**SEP-837 — set `application_type` correctly when overriding the heuristic.** The SDK defaults `clientMetadata.application_type` from your `redirect_uris` (loopback / custom scheme → `'native'`, else `'web'`). When the heuristic is wrong for your deployment — a web app dev-served on `localhost`, a native app with an `https:` claimed redirect — set the field explicitly; the SDK never overwrites a value you set but cannot know your deployment shape. See [Dynamic Client Registration defaults](#dynamic-client-registration-application_type-and-grant_types-defaults-sep-837-sep-2207).
1662
+
1663
+
-**SEP-2350 — track cross-request step-up failures yourself.** The SDK caps step-up retries **per request** (`maxStepUpRetries`). Tracking "this (resource, operation) has already failed step-up _N_ times across the session" — to back off, surface an error, or stop prompting the user — is host state the SDK has no visibility into. See the `client-auth:stepup:*` scenarios in [`test/e2e/scenarios/client-auth.test.ts`](../test/e2e/scenarios/client-auth.test.ts) for the transport-driven step-up flow.
1664
+
1665
+
-**SEP-2207 (resource-server operators) — do not advertise `offline_access` from the RS.** A resource server SHOULD NOT include `offline_access` in its `WWW-Authenticate``scope` challenge or in its protected-resource metadata `scopes_supported` — refresh-token issuance is between the client and the authorization server. This is operator configuration of whatever serves your `WWW-Authenticate` header and PRM document, not SDK code.
0 commit comments