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
Wires the existing CrossAppAccessProvider into the auth/enterprise-managed-authorization
conformance fixture (8/8 checks pass, zero SDK code change). Consolidates the per-PR
migration.md sections into a single 2026-authorization conformance contract aligned with
the issuer-stamp model, and refreshes the clientGuide auth snippets + docs/client.md.
Reconciles expected-failures baselines with the published 0.2.0-alpha.5 referee.
The MyOAuthProvider guide example now implements state() and discoveryState()/
saveDiscoveryState() so the documented state-check is reachable and the callback-leg
AS-binding is demonstrated; the deprecated saveAuthorizationServerUrl pair is dropped.
The auth_finishAuth snippet now wires the CSRF check to provider.lastState and
reconnects on a fresh StreamableHTTPClientTransport (a started transport cannot be
restarted). Unused Prompt/Resource/Tool type imports dropped from the synced imports
region.
auth/* is fully green on both legs at the tip. Two client baseline entries remain at
the alpha.5 pin: json-schema-ref-no-deref (SEP-2106 territory, unrelated) and
auth/2025-03-26-oauth-metadata-backcompat (referee mock still serves a §3.3-violating
issuer at this pin; tracked for a referee-side fix or removal at the next conformance
pin bump).
Claude-Session: https://claude.ai/code/session_01XBib5gRe8AMPPJhySCz3EJ
@@ -191,9 +201,111 @@ 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
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):
1514
+
The inline options object on `auth()` is now the named `AuthOptions` type, exported from `@modelcontextprotocol/client`. Existing call sites need no change. New fields:
1515
1515
1516
-
-`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.
1517
-
-`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`.
1516
+
-`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.
1517
+
-`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`.
1518
+
-`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).
1518
1519
1519
1520
### `OAuthClientProvider` credential methods receive an `issuer` context
1520
1521
@@ -1574,13 +1575,21 @@ The bundled `ClientCredentialsProvider`, `PrivateKeyJwtProvider`, `StaticPrivate
1574
1575
1575
1576
### Conformance obligations for `OAuthClientProvider` implementers
1576
1577
1577
-
<!-- Filled in as the SEP-2352/2350/837/2207 behavior PRs land. -->
1578
+
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. 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.
1580
1581
1581
-
**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.
1582
+
-**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.
1582
1583
1583
-
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`.
1584
+
-**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.
1585
+
1586
+
-**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.
1587
+
1588
+
-**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).
1589
+
1590
+
-**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 [`examples/scoped-tools/client.ts`](../examples/scoped-tools/client.ts) for the per-request step-up flow.
1591
+
1592
+
-**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