Re-key the persisted OAuth store to (server, issuer) and add the
SEP-837/2350/2352 authorization-hardening UX. The SDK v2 auth() drives the
SEP-2352 flow off the provider's ctx.issuer; the Inspector's job is to honor it
and round-trip the issuer stamp.
SEP-2352 — issuer-bound credentials
- ServerOAuthState nests issuer-bound credentials under
byIssuer[issuer] = { clientInformation, clientRegistrationKind, tokens }, with
an activeIssuer pointer answering the transport's ctx-less per-request bearer
read.
- Fix a latent bug: OAuthStorageBase re-parsed tokens/client-info through
OAuthTokensSchema/OAuthClientInformationSchema, which strip the non-RFC
`issuer` field and silently defeat the SDK's discardIfIssuerMismatch. The
store re-attaches the stamp from the byIssuer key.
- BaseOAuthClientProvider threads ctx.issuer and implements
discoveryState()/saveDiscoveryState() (activates the SDK's callback-leg
AuthorizationServerMismatchError binding check) and invalidateCredentials().
- Lazy, migration-free upgrade: a legacy pre-1625 blob's top-level
clientInformation/tokens are returned as an unkeyed fallback until the first
issuer-stamped save promotes them into byIssuer.
SEP-837 — application_type
- DCR declares application_type: "native" (localhost Inspector).
- ClientSettingsForm renders RegistrationRejectedError detail and reflects DCR's
deprecated-in-favor-of-CIMD (SEP-991) status.
SEP-2350 — step-up scope union
- Per-server onInsufficientScope (reauthorize | throw) setting wired
ServerSettingsForm -> StoredMCPServer.oauth -> StreamableHTTPClientTransport.
- StepUpAuthModal visualizes the prior-union-challenged scope set, tagging each
scope `new` vs `already granted`.
mcpAuth simplification
- Delete the hand-rolled authorizeWithoutRefresh (mishandled issuer stamping);
forward forceReauthorization/skipIssuerMetadataValidation to native SDK auth()
so both the initial and step-up paths get SEP-2352 stamping.
Vocabulary + docs
- OAuthStep gains cimd_fetch / issuer_comparison / scope_step_up (recorded on a
step-up redirect); the Network tab labels auth requests by OAuth phase.
- v2_auth_hardening.md gains an as-built status section.
Deferred to the era-aware card: the SdkError(EraNegotiationFailed) ->
UnauthorizedError unwrap and finishAuth(URLSearchParams) form (only fire under
non-legacy negotiation).
Closes #1625
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
/**
|
|
* Thin wrapper over the `@modelcontextprotocol/client` v2 OAuth orchestrator.
|
|
*
|
|
* SDK v2's `auth()` handles the full flow natively — including `forceReauthorization`
|
|
* (skip refresh and start a fresh authorization request when a step-up scope union
|
|
* cannot be widened by refresh, RFC 6749 §6), RFC 9207 `iss` validation, SEP-2352
|
|
* issuer stamping/`ctx.issuer` keying, and SEP-837 `application_type` defaults. This
|
|
* wrapper exists only to pin the option shape (`McpAuthOptions`) the Inspector passes
|
|
* and to keep a single import site; it no longer reimplements any flow logic.
|
|
*/
|
|
|
|
import { auth as sdkAuth } from "@modelcontextprotocol/client";
|
|
import type { OAuthClientProvider } from "@modelcontextprotocol/client";
|
|
import type { FetchLike } from "@modelcontextprotocol/client";
|
|
|
|
export type McpAuthResult = "AUTHORIZED" | "REDIRECT";
|
|
|
|
/**
|
|
* Options forwarded to SDK v2 `auth()`.
|
|
*/
|
|
export interface McpAuthOptions {
|
|
serverUrl: string | URL;
|
|
authorizationCode?: string;
|
|
/** RFC 9207 callback `iss`, validated by the SDK against the metadata issuer. */
|
|
iss?: string;
|
|
scope?: string;
|
|
resourceMetadataUrl?: URL;
|
|
fetchFn?: FetchLike;
|
|
/** SEP-2468 opt-out for the RFC 8414 §3.3 issuer-echo check (security-weakening). */
|
|
skipIssuerMetadataValidation?: boolean;
|
|
/**
|
|
* Skip refresh and start an authorization-code flow. Required for step-up when
|
|
* the union scope exceeds the current token grant (RFC 6749 §6).
|
|
*/
|
|
forceReauthorization?: boolean;
|
|
}
|
|
|
|
export async function mcpAuth(
|
|
provider: OAuthClientProvider,
|
|
options: McpAuthOptions,
|
|
): Promise<McpAuthResult> {
|
|
if (options.forceReauthorization && options.authorizationCode !== undefined) {
|
|
throw new Error(
|
|
"forceReauthorization cannot be combined with authorizationCode",
|
|
);
|
|
}
|
|
|
|
return sdkAuth(provider, {
|
|
serverUrl: options.serverUrl,
|
|
authorizationCode: options.authorizationCode,
|
|
iss: options.iss,
|
|
scope: options.scope,
|
|
resourceMetadataUrl: options.resourceMetadataUrl,
|
|
fetchFn: options.fetchFn,
|
|
skipIssuerMetadataValidation: options.skipIssuerMetadataValidation,
|
|
forceReauthorization: options.forceReauthorization,
|
|
});
|
|
}
|