OAuth: re-key store to (server, issuer) + SEP-837/2350/2352 auth UX (#1694)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* 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> * Address @claude review: issuer-stamp source gating + clear/activeIssuer + phase over-match - SEP-2352: stamp the issuer only when the credential actually came from the byIssuer slot (`fromSlot`), not merely when a slot exists. A partially-populated slot alongside a surviving legacy field would otherwise stamp a legacy (possibly cross-AS) credential with the current issuer, defeating discardIfIssuerMismatch. - Issuer-scoped clears no longer promote the cleared issuer to `activeIssuer` (updateIssuerSlot gains a `setActive` flag; save paths keep the promotion). - oauthNetworkPhase matches register/token/authorize as the final path segment (trailing slash tolerated) so `/token/refresh` or `/api/register/foo` no longer over-match. - Tests for all three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f915889fc4
commit
7fecebed5e
1 changed files with 1620 additions and 601 deletions
|
|
@ -11,6 +11,17 @@ The [2026-07-28 release candidate](https://blog.modelcontextprotocol.io/posts/20
|
|||
|
||||
**Strategy:** Upgrade to the v2 TypeScript SDK (`@modelcontextprotocol/client`) when its auth-hardening PRs land. Do **not** reimplement SDK OAuth logic in Inspector — delegate connect-time standard OAuth to the SDK and wire the gaps (callback parameters, storage shape, client type). **Mid-session authorization** is a separate, near-term track ([Mid-session auth](v2_auth_mid_session.md)); this doc covers connect-time and storage hardening that mid-session builds on.
|
||||
|
||||
## Implementation status (#1625, on `@modelcontextprotocol/client` 2.0.0-beta.4)
|
||||
|
||||
The SDK upgrade ([#1624](https://github.com/modelcontextprotocol/inspector/issues/1624)) and RFC 9207 `iss` forwarding ([#1687](https://github.com/modelcontextprotocol/inspector/pull/1687)) landed first. This section records the **as-built** behavior of the storage re-key + SEP-837/2350/2352 UX ([#1625](https://github.com/modelcontextprotocol/inspector/issues/1625)); it supersedes the forward-looking prose below where they differ.
|
||||
|
||||
- **SEP-2352 — issuer-bound credentials (done).** `ServerOAuthState` now nests issuer-bound credentials under `byIssuer[issuer] = { clientInformation, clientRegistrationKind, tokens }`, with an `activeIssuer` pointer answering the transport's ctx-less per-request bearer read. The SDK v2 `auth()` drives the whole flow — it builds `infoCtx = { issuer }`, stamps `issuer` on everything it saves, and calls `discardIfIssuerMismatch` on reads — so the Inspector's job is to **honor `ctx.issuer`** and **round-trip the stamp**. A latent bug was that `OAuthStorageBase` re-parsed tokens/client-info through `OAuthTokensSchema`/`OAuthClientInformationSchema`, which **strip** the non-RFC `issuer` field; the store now re-attaches it from the `byIssuer` key. `BaseOAuthClientProvider` implements `discoveryState()`/`saveDiscoveryState()` (activating the SDK's callback-leg `AuthorizationServerMismatchError` binding check) and `invalidateCredentials(scope)`. Migration is **lazy**: 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` — existing web/TUI/CLI sessions survive the upgrade.
|
||||
- **SEP-837 — `application_type` (done).** The shared `BaseOAuthClientProvider` declares `application_type: "native"` for **all** environments: the Inspector is a locally-hosted app reached over `localhost`, so it is a native client per RFC 8252 (the SDK also infers `"native"` from loopback `redirect_uris`). This supersedes the "web = `web`" split in the per-SEP table below. `ClientSettingsForm` renders DCR `RegistrationRejectedError` details and reflects DCR's deprecated-in-favor-of-CIMD (SEP-991) status.
|
||||
- **SEP-2350 — step-up union (done).** The scope-union machinery (`core/auth/scopes.ts`, `handleAuthChallenge`) predates this card; #1625 adds the UX: a **per-server** `onInsufficientScope` (`reauthorize | throw`) setting (`ServerSettingsForm` → `StoredMCPServer.oauth` → `StreamableHTTPClientTransport`), and a scope-union visualization in `StepUpAuthModal` that tags each union scope `new` vs `already granted`.
|
||||
- **`mcpAuth` (simplified, not deleted).** `core/auth/mcpAuth.ts` is now a thin forwarder to SDK `auth()` — the hand-rolled `authorizeWithoutRefresh` (which mishandled issuer stamping) was deleted and `forceReauthorization`/`skipIssuerMetadataValidation` are forwarded natively, so both the initial and step-up paths get SEP-2352 stamping. `core/auth/scopes.ts` is **retained** (still the union source; the SDK's `computeScopeUnion` export is a future cleanup).
|
||||
- **Vocabulary.** `OAuthStep` gains `cimd_fetch` / `issuer_comparison` / `scope_step_up` (the manager records `scope_step_up` on a step-up redirect), and the Network tab labels `auth`-category requests by OAuth phase (Discovery / Registration / Authorize / Token).
|
||||
- **Deferred to the era-aware card.** The `SdkError(EraNegotiationFailed) → UnauthorizedError` unwrap and the `finishAuth(URLSearchParams)` whole-params form only fire under `auto`/`pin` negotiation; the Inspector is pinned to `versionNegotiation: 'legacy'`, so this lands with the era-aware connection model.
|
||||
|
||||
## Normative references
|
||||
|
||||
- [MCP authorization (draft — 2026-07-28 RC target)](https://modelcontextprotocol.io/specification/draft/basic/authorization)
|
||||
|
|
|
|||
Loading…
Reference in a new issue