OAuth: re-key store to (server, issuer) + SEP-837/2350/2352 auth UX (#1625)
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>
This commit is contained in:
parent
f915889fc4
commit
5feebf25c2
1 changed files with 1527 additions and 601 deletions
|
|
@ -7,6 +7,7 @@ import type {
|
|||
OAuthClientInformation,
|
||||
OAuthTokens,
|
||||
OAuthMetadata,
|
||||
OAuthDiscoveryState,
|
||||
} from "@modelcontextprotocol/client";
|
||||
import type {
|
||||
IdpSessionState,
|
||||
|
|
@ -15,19 +16,57 @@ import type {
|
|||
import type { OAuthPersistSnapshot } from "./oauth-persist.js";
|
||||
|
||||
/**
|
||||
* OAuth state for a single server
|
||||
* OAuth credentials bound to a single authorization-server `issuer` (SEP-2352).
|
||||
*
|
||||
* Client identifiers are unique to the AS that issued them (RFC 6749 §2.2), and
|
||||
* tokens minted by one AS must never be presented to another. The SDK v2 stamps
|
||||
* `issuer` on the objects it saves and passes `ctx.issuer` on every read/write;
|
||||
* this record is how the Inspector keys that state per authorization server so a
|
||||
* resource that migrates between ASes re-registers/re-authorizes cleanly instead
|
||||
* of reusing mismatched credentials.
|
||||
*/
|
||||
export interface ServerOAuthState {
|
||||
export interface IssuerBoundOAuthState {
|
||||
clientInformation?: OAuthClientInformation;
|
||||
/** Set when {@link clientInformation} is saved — DCR vs CIMD. */
|
||||
clientRegistrationKind?: OAuthClientRegistrationKind;
|
||||
preregisteredClientInformation?: OAuthClientInformation;
|
||||
tokens?: OAuthTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth state for a single server.
|
||||
*
|
||||
* Issuer-bound credentials (client info + tokens) live under {@link byIssuer},
|
||||
* keyed by AS `issuer` (SEP-2352). Per-server / per-flow state (code verifier,
|
||||
* discovered metadata, discovery state, static client info, EMA marker, and the
|
||||
* requested-scope seed) is issuer-independent and stays at the top level.
|
||||
*
|
||||
* The bare top-level {@link clientInformation} / {@link tokens} fields are the
|
||||
* **legacy unkeyed fallback**: a pre-1625 snapshot deserializes with credentials
|
||||
* here and no `issuer` stamp. Reads fall back to them when {@link byIssuer} has
|
||||
* no entry; the first issuer-stamped save promotes them into {@link byIssuer} and
|
||||
* clears the fallback (lazy migration — see `OAuthStorageBase`). New saves never
|
||||
* write the bare fields.
|
||||
*/
|
||||
export interface ServerOAuthState {
|
||||
/** Per-issuer credentials (SEP-2352). */
|
||||
byIssuer?: Record<string, IssuerBoundOAuthState>;
|
||||
/** Most-recently-saved issuer — answers ctx-less reads (per-request bearer token). */
|
||||
activeIssuer?: string;
|
||||
preregisteredClientInformation?: OAuthClientInformation;
|
||||
codeVerifier?: string;
|
||||
scope?: string;
|
||||
serverMetadata?: OAuthMetadata;
|
||||
/** RFC 9728/8414 discovery cache; persisted alongside {@link codeVerifier} (SEP-2352 callback-leg binding). */
|
||||
discoveryState?: OAuthDiscoveryState;
|
||||
/** Set when resource tokens were minted via EMA (legs 2–3). */
|
||||
enterpriseManaged?: boolean;
|
||||
|
||||
/** @deprecated Legacy unkeyed fallback — see {@link ServerOAuthState}. */
|
||||
clientInformation?: OAuthClientInformation;
|
||||
/** @deprecated Legacy unkeyed fallback — see {@link ServerOAuthState}. */
|
||||
clientRegistrationKind?: OAuthClientRegistrationKind;
|
||||
/** @deprecated Legacy unkeyed fallback — see {@link ServerOAuthState}. */
|
||||
tokens?: OAuthTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue