Skip to content

Commit ec501fa

Browse files
feat(client,core): auth surface delta — AuthOptions, issuer ctx/stamp, authErrors module
Surface delta for the 2026-07-28 authorization requirements. All additive; existing OAuthClientProvider implementers compile unchanged. The new fields are inert until the behavior changes that follow wire them up. - Extract the inline auth() options object as exported AuthOptions and add iss (RFC 9207 callback parameter) and skipIssuerMetadataValidation (RFC 8414 §3.3 opt-out). JSDoc is non-assertive — validation lands in the follow-up commit. - OAuthClientProvider.clientInformation/saveClientInformation/tokens/ saveTokens accept an optional OAuthClientInformationContext carrying the resolved authorization-server issuer so providers can key persisted credentials per AS. - OAuthTokens and OAuthClientInformation gain an optional issuer stamp field (core/shared/auth.ts) — the slot the SDK writes before persistence so stored credentials are bound to the AS that issued them. - New packages/client/src/client/authErrors.ts with the OAuthClientFlowError base class; the flow-specific error classes from later commits land here. Claude-Session: https://claude.ai/code/session_01XBib5gRe8AMPPJhySCz3EJ
1 parent 3d88907 commit ec501fa

7 files changed

Lines changed: 158 additions & 29 deletions

File tree

‎.changeset/auth-surface-delta.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/client': minor
3+
'@modelcontextprotocol/core': minor
4+
---
5+
6+
Add the public surface for the 2026-07-28 authorization requirements. New `AuthOptions` type names the `auth()` options object and adds `iss` and `skipIssuerMetadataValidation` fields. `OAuthClientProvider.clientInformation()` / `.saveClientInformation()` / `.tokens()` / `.saveTokens()` accept an optional `OAuthClientInformationContext` carrying the authorization server's `issuer` so providers can key persisted credentials per authorization server. New `StoredOAuthTokens` / `StoredOAuthClientInformation` aliases add an `issuer` stamp field on top of the wire types (kept off the wire schemas so an authorization server cannot populate it) and become the parameter/return types of the credential methods. New `OAuthClientFlowError` base class in `authErrors.ts` for the flow-specific error classes that follow. All changes are additive — existing `OAuthClientProvider` implementations compile unchanged; the new fields are inert until the behavior changes that follow wire them up.

‎docs/migration.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,27 @@ The following APIs are unchanged between v1 and v2 (only the import paths change
14741474
`Session not found` — unchanged from v1. Note that this use of `-32001` is an SDK convention, not a spec-assigned error code, and it is expected to be re-derived as error handling for the 2026 protocol revision (`2026-07-28`) is adopted. Avoid hard-coding the `-32001` code in
14751475
client logic; key off the HTTP `404` status instead.
14761476

1477+
## Authorization (2026-07-28 spec)
1478+
1479+
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).
1480+
1481+
### `auth()` options are now `AuthOptions`
1482+
1483+
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):
1484+
1485+
- `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.
1486+
- `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`.
1487+
1488+
### `OAuthClientProvider` credential methods receive an `issuer` context
1489+
1490+
`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.
1491+
1492+
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.
1493+
1494+
### Conformance obligations for `OAuthClientProvider` implementers
1495+
1496+
<!-- Filled in as the SEP-2468/2352/2350/837/2207 behavior PRs land. -->
1497+
14771498
## Using an LLM to migrate your code
14781499

14791500
An LLM-optimized version of this guide is available at [`docs/migration-SKILL.md`](migration-SKILL.md). It contains dense mapping tables designed for tools like Claude Code to mechanically apply all the changes described above. You can paste it into your LLM context or load it as

‎packages/client/src/client/auth.ts‎

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import type {
88
OAuthClientMetadata,
99
OAuthMetadata,
1010
OAuthProtectedResourceMetadata,
11-
OAuthTokens
11+
OAuthTokens,
12+
StoredOAuthClientInformation,
13+
StoredOAuthTokens
1214
} from '@modelcontextprotocol/core';
1315
import {
1416
checkResourceAllowed,
@@ -82,6 +84,22 @@ export interface AuthProvider {
8284
onUnauthorized?(ctx: UnauthorizedContext): Promise<void>;
8385
}
8486

87+
/**
88+
* Context passed to the credential-persistence methods on
89+
* {@linkcode OAuthClientProvider} — `clientInformation` / `saveClientInformation`
90+
* and `tokens` / `saveTokens`. Carries the resolved authorization-server `issuer`
91+
* so provider implementations can key persisted credentials per authorization
92+
* server (RFC 6749 §2.2 — client identifiers are unique to the AS that issued
93+
* them). Providers that store a single credential set may ignore it.
94+
*/
95+
export interface OAuthClientInformationContext {
96+
/**
97+
* The authorization server's `issuer` identifier from its validated metadata
98+
* document, used as the binding key for persisted credentials.
99+
*/
100+
issuer: string;
101+
}
102+
85103
/**
86104
* Type guard distinguishing `OAuthClientProvider` from a minimal `AuthProvider`.
87105
* Transports use this at construction time to classify the `authProvider` option.
@@ -167,8 +185,14 @@ export interface OAuthClientProvider {
167185
* Loads information about this OAuth client, as registered already with the
168186
* server, or returns `undefined` if the client is not registered with the
169187
* server.
188+
*
189+
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
190+
* that persist credentials per authorization server should return the entry
191+
* keyed by `ctx.issuer`. Providers with a single credential set may ignore it.
170192
*/
171-
clientInformation(): OAuthClientInformationMixed | undefined | Promise<OAuthClientInformationMixed | undefined>;
193+
clientInformation(
194+
ctx?: OAuthClientInformationContext
195+
): StoredOAuthClientInformation | undefined | Promise<StoredOAuthClientInformation | undefined>;
172196

173197
/**
174198
* If implemented, this permits the OAuth client to dynamically register with
@@ -177,20 +201,32 @@ export interface OAuthClientProvider {
177201
*
178202
* This method is not required to be implemented if client information is
179203
* statically known (e.g., pre-registered).
204+
*
205+
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
206+
* that persist credentials per authorization server should store the entry
207+
* keyed by `ctx.issuer`.
180208
*/
181-
saveClientInformation?(clientInformation: OAuthClientInformationMixed): void | Promise<void>;
209+
saveClientInformation?(clientInformation: StoredOAuthClientInformation, ctx?: OAuthClientInformationContext): void | Promise<void>;
182210

183211
/**
184212
* Loads any existing OAuth tokens for the current session, or returns
185213
* `undefined` if there are no saved tokens.
214+
*
215+
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
216+
* that persist tokens per authorization server should return the entry
217+
* keyed by `ctx.issuer`. Providers with a single token set may ignore it.
186218
*/
187-
tokens(): OAuthTokens | undefined | Promise<OAuthTokens | undefined>;
219+
tokens(ctx?: OAuthClientInformationContext): StoredOAuthTokens | undefined | Promise<StoredOAuthTokens | undefined>;
188220

189221
/**
190222
* Stores new OAuth tokens for the current session, after a successful
191223
* authorization.
224+
*
225+
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
226+
* that persist tokens per authorization server should store the entry
227+
* keyed by `ctx.issuer`.
192228
*/
193-
saveTokens(tokens: OAuthTokens): void | Promise<void>;
229+
saveTokens(tokens: StoredOAuthTokens, ctx?: OAuthClientInformationContext): void | Promise<void>;
194230

195231
/**
196232
* Invoked to redirect the user agent to the given URL to begin the authorization flow.
@@ -531,22 +567,50 @@ export async function parseErrorResponse(input: Response | string): Promise<OAut
531567
}
532568
}
533569

570+
/**
571+
* Options for {@linkcode auth}. The full OAuth flow orchestrator's input.
572+
*/
573+
export interface AuthOptions {
574+
/** The MCP server URL — the protected resource the flow authorizes against. */
575+
serverUrl: string | URL;
576+
/**
577+
* The authorization code returned by the authorization server on the redirect
578+
* callback. When set, {@linkcode auth} exchanges it for tokens; when unset,
579+
* {@linkcode auth} runs discovery and either refreshes or initiates redirect.
580+
*/
581+
authorizationCode?: string;
582+
/**
583+
* The form-urldecoded `iss` query parameter from the authorization callback,
584+
* if present. Passed through to RFC 9207 §2.4 issuer validation alongside
585+
* `authorizationCode`. The validation behavior is wired up in a follow-up
586+
* change; this field is currently inert.
587+
*/
588+
iss?: string;
589+
/** Scope to request; computed by Scope Selection Strategy when omitted. */
590+
scope?: string;
591+
/** Explicit `resource_metadata` URL from a `WWW-Authenticate` challenge. */
592+
resourceMetadataUrl?: URL;
593+
/** Custom `fetch` implementation. */
594+
fetchFn?: FetchLike;
595+
/**
596+
* Opt-out for the RFC 8414 §3.3 issuer-echo check during authorization
597+
* server discovery. Disabling it is **security-weakening** and intended only
598+
* for authorization servers known to publish a mismatched `issuer`. The
599+
* check itself is wired up in a follow-up change; this flag is currently
600+
* inert.
601+
*
602+
* @default false
603+
*/
604+
skipIssuerMetadataValidation?: boolean;
605+
}
606+
534607
/**
535608
* Orchestrates the full auth flow with a server.
536609
*
537610
* This can be used as a single entry point for all authorization functionality,
538611
* instead of linking together the other lower-level functions in this module.
539612
*/
540-
export async function auth(
541-
provider: OAuthClientProvider,
542-
options: {
543-
serverUrl: string | URL;
544-
authorizationCode?: string;
545-
scope?: string;
546-
resourceMetadataUrl?: URL;
547-
fetchFn?: FetchLike;
548-
}
549-
): Promise<AuthResult> {
613+
export async function auth(provider: OAuthClientProvider, options: AuthOptions): Promise<AuthResult> {
550614
try {
551615
return await authInternal(provider, options);
552616
} catch (error) {
@@ -600,19 +664,7 @@ export function determineScope(options: {
600664

601665
async function authInternal(
602666
provider: OAuthClientProvider,
603-
{
604-
serverUrl,
605-
authorizationCode,
606-
scope,
607-
resourceMetadataUrl,
608-
fetchFn
609-
}: {
610-
serverUrl: string | URL;
611-
authorizationCode?: string;
612-
scope?: string;
613-
resourceMetadataUrl?: URL;
614-
fetchFn?: FetchLike;
615-
}
667+
{ serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }: AuthOptions
616668
): Promise<AuthResult> {
617669
// Check if the provider has cached discovery state to skip discovery
618670
const cachedState = await provider.discoveryState?.();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Error classes thrown by the OAuth client flow ({@linkcode auth} and helpers).
3+
*
4+
* Each behavior change in the 2026-07-28 authorization requirements adds its
5+
* dedicated error class to this module so callers can `instanceof`-dispatch on
6+
* the failure mode without string-matching messages.
7+
*/
8+
9+
/**
10+
* Base class for the OAuth-client-flow error family. Concrete subclasses are
11+
* added to this module alongside the SEP-2468/837/2207/2350/2352 behavior
12+
* changes that throw them, so callers can catch the whole family with a single
13+
* `instanceof OAuthClientFlowError` guard once those land.
14+
*
15+
* @remarks Nothing in the SDK throws this base class directly. In the release
16+
* that introduces it no subclass exists yet — the guard is a forward-compat
17+
* hook and will not match anything until the first behavior change ships.
18+
*/
19+
export class OAuthClientFlowError extends Error {
20+
constructor(message: string) {
21+
super(message);
22+
this.name = new.target.name;
23+
}
24+
}

‎packages/client/src/index.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
export type {
1010
AddClientAuthentication,
11+
AuthOptions,
1112
AuthProvider,
1213
AuthResult,
1314
ClientAuthMethod,
15+
OAuthClientInformationContext,
1416
OAuthClientProvider,
1517
OAuthDiscoveryState,
1618
OAuthServerInfo
@@ -37,6 +39,7 @@ export {
3739
UnauthorizedError,
3840
validateClientMetadataUrl
3941
} from './client/auth.js';
42+
export { OAuthClientFlowError } from './client/authErrors.js';
4043
export type {
4144
AssertionCallback,
4245
ClientCredentialsProviderOptions,

‎packages/core/src/exports/public/index.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export type {
3030
OAuthTokenRevocationRequest,
3131
OAuthTokens,
3232
OpenIdProviderDiscoveryMetadata,
33-
OpenIdProviderMetadata
33+
OpenIdProviderMetadata,
34+
StoredOAuthClientInformation,
35+
StoredOAuthTokens
3436
} from '../../shared/auth.js';
3537

3638
// Auth utilities

‎packages/core/src/shared/auth.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,27 @@ export type OAuthClientMetadata = z.infer<typeof OAuthClientMetadataSchema>;
244244
export type OAuthClientInformation = z.infer<typeof OAuthClientInformationSchema>;
245245
export type OAuthClientInformationFull = z.infer<typeof OAuthClientInformationFullSchema>;
246246
export type OAuthClientInformationMixed = OAuthClientInformation | OAuthClientInformationFull;
247+
248+
/**
249+
* {@linkcode OAuthTokens} as persisted by an `OAuthClientProvider`. Adds an
250+
* SDK-stamped authorization-server `issuer` identifier so stored tokens are
251+
* bound to the AS that issued them. The `issuer` field is **not** part of the
252+
* RFC 6749 wire response and is intentionally absent from
253+
* {@linkcode OAuthTokensSchema}; the client SDK writes it before calling
254+
* `saveTokens`.
255+
*/
256+
export type StoredOAuthTokens = OAuthTokens & { issuer?: string };
257+
258+
/**
259+
* {@linkcode OAuthClientInformationMixed} as persisted by an
260+
* `OAuthClientProvider`. Adds an SDK-stamped authorization-server `issuer`
261+
* identifier so stored client credentials are bound to the AS that issued them.
262+
* The `issuer` field is **not** part of the RFC 7591 wire response and is
263+
* intentionally absent from {@linkcode OAuthClientInformationSchema}; the
264+
* client SDK writes it before calling `saveClientInformation`.
265+
*/
266+
export type StoredOAuthClientInformation = OAuthClientInformationMixed & { issuer?: string };
267+
247268
export type OAuthClientRegistrationError = z.infer<typeof OAuthClientRegistrationErrorSchema>;
248269
export type OAuthTokenRevocationRequest = z.infer<typeof OAuthTokenRevocationRequestSchema>;
249270
export type OAuthProtectedResourceMetadata = z.infer<typeof OAuthProtectedResourceMetadataSchema>;

0 commit comments

Comments
 (0)