Skip to content

Commit d1ffd2f

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 d1ffd2f

6 files changed

Lines changed: 147 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. `OAuthTokens` and `OAuthClientInformation` gain an optional `issuer` stamp field. 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+
`OAuthTokens` and `OAuthClientInformation` also gain an optional `issuer?: string` field. Once the SEP-2352 behavior change lands the SDK will stamp this 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: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ export interface AuthProvider {
8282
onUnauthorized?(ctx: UnauthorizedContext): Promise<void>;
8383
}
8484

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

173195
/**
174196
* If implemented, this permits the OAuth client to dynamically register with
@@ -177,20 +199,32 @@ export interface OAuthClientProvider {
177199
*
178200
* This method is not required to be implemented if client information is
179201
* statically known (e.g., pre-registered).
202+
*
203+
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
204+
* that persist credentials per authorization server should store the entry
205+
* keyed by `ctx.issuer`.
180206
*/
181-
saveClientInformation?(clientInformation: OAuthClientInformationMixed): void | Promise<void>;
207+
saveClientInformation?(clientInformation: OAuthClientInformationMixed, ctx?: OAuthClientInformationContext): void | Promise<void>;
182208

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

189219
/**
190220
* Stores new OAuth tokens for the current session, after a successful
191221
* authorization.
222+
*
223+
* @param ctx - Carries the resolved authorization-server `issuer`. Providers
224+
* that persist tokens per authorization server should store the entry
225+
* keyed by `ctx.issuer`.
192226
*/
193-
saveTokens(tokens: OAuthTokens): void | Promise<void>;
227+
saveTokens(tokens: OAuthTokens, ctx?: OAuthClientInformationContext): void | Promise<void>;
194228

195229
/**
196230
* Invoked to redirect the user agent to the given URL to begin the authorization flow.
@@ -531,22 +565,50 @@ export async function parseErrorResponse(input: Response | string): Promise<OAut
531565
}
532566
}
533567

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

601663
async function authInternal(
602664
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-
}
665+
{ serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }: AuthOptions
616666
): Promise<AuthResult> {
617667
// Check if the provider has cached discovery state to skip discovery
618668
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/shared/auth.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,14 @@ export const OAuthTokensSchema = z
135135
token_type: z.string(),
136136
expires_in: z.coerce.number().optional(),
137137
scope: z.string().optional(),
138-
refresh_token: z.string().optional()
138+
refresh_token: z.string().optional(),
139+
/**
140+
* SDK-stamped authorization-server `issuer` identifier these tokens were
141+
* obtained from. Not part of the RFC 6749 wire response; the client SDK
142+
* writes it before persistence so stored tokens are bound to the AS that
143+
* issued them.
144+
*/
145+
issuer: z.string().optional()
139146
})
140147
.strip();
141148

@@ -205,7 +212,14 @@ export const OAuthClientInformationSchema = z
205212
client_id: z.string(),
206213
client_secret: z.string().optional(),
207214
client_id_issued_at: z.number().optional(),
208-
client_secret_expires_at: z.number().optional()
215+
client_secret_expires_at: z.number().optional(),
216+
/**
217+
* SDK-stamped authorization-server `issuer` identifier this client was
218+
* registered with. Not part of the RFC 7591 wire response; the client SDK
219+
* writes it before persistence so stored client credentials are bound to
220+
* the AS that issued them.
221+
*/
222+
issuer: z.string().optional()
209223
})
210224
.strip();
211225

0 commit comments

Comments
 (0)