inspector/core/auth/store.ts
Bob Dickinson 5de4e911e3
feat(web,core): enterprise-managed authorization (EMA) (#1509) (#1512)
Adds enterprise-managed authorization (EMA): browser-side IdP login + RFC 8693 token-exchange and JWT-bearer resource-token mint, per-server EMA opt-in in Server Settings, IdP config in Client Settings, and IdP clientSecret keychain storage. Includes the v2/main merge and review follow-ups (400 on malformed client config; normalizeIdpIssuer import consolidation).
2026-06-24 22:15:12 -04:00

122 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* OAuth store factory using Zustand.
* Creates a store with any storage adapter (file, remote, sessionStorage).
*/
import { createStore } from "zustand/vanilla";
import { persist, createJSONStorage } from "zustand/middleware";
import type {
OAuthClientInformation,
OAuthTokens,
OAuthMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import type { IdpSessionState } from "./storage.js";
/**
* OAuth state for a single server
*/
export interface ServerOAuthState {
clientInformation?: OAuthClientInformation;
preregisteredClientInformation?: OAuthClientInformation;
tokens?: OAuthTokens;
codeVerifier?: string;
scope?: string;
serverMetadata?: OAuthMetadata;
/** Set when resource tokens were minted via EMA (legs 2–3). */
enterpriseManaged?: boolean;
}
/**
* Zustand store state (all servers)
*/
export interface OAuthStoreState {
servers: Record<string, ServerOAuthState>;
idpSessions: Record<string, IdpSessionState>;
getServerState: (serverUrl: string) => ServerOAuthState;
setServerState: (serverUrl: string, state: Partial<ServerOAuthState>) => void;
clearServerState: (serverUrl: string) => void;
getIdpSession: (issuer: string) => IdpSessionState;
setIdpSession: (issuer: string, updates: Partial<IdpSessionState>) => void;
clearIdpSession: (issuer: string) => void;
clearEnterpriseManagedResourceServers: () => void;
}
/**
* Creates a Zustand store for OAuth state with the given storage adapter.
* The storage adapter handles persistence (file, remote HTTP, sessionStorage, etc.).
*
* @param storage - Zustand storage adapter (from createJSONStorage)
* @returns Zustand store instance
*/
export function createOAuthStore(
storage: ReturnType<typeof createJSONStorage>,
) {
return createStore<OAuthStoreState>()(
persist(
(set, get) => ({
servers: {},
idpSessions: {},
getServerState: (serverUrl: string) => {
return get().servers[serverUrl] || {};
},
setServerState: (
serverUrl: string,
updates: Partial<ServerOAuthState>,
) => {
set((state) => ({
servers: {
...state.servers,
[serverUrl]: {
...state.servers[serverUrl],
...updates,
},
},
}));
},
clearServerState: (serverUrl: string) => {
set((state) => {
const rest = { ...state.servers };
delete rest[serverUrl];
return { servers: rest };
});
},
getIdpSession: (issuer: string) => {
return get().idpSessions[issuer] || {};
},
setIdpSession: (issuer: string, updates: Partial<IdpSessionState>) => {
set((state) => ({
idpSessions: {
...state.idpSessions,
[issuer]: {
...state.idpSessions[issuer],
...updates,
},
},
}));
},
clearIdpSession: (issuer: string) => {
set((state) => {
const rest = { ...state.idpSessions };
delete rest[issuer];
return { idpSessions: rest };
});
},
clearEnterpriseManagedResourceServers: () => {
set((state) => {
const rest = { ...state.servers };
for (const [url, serverState] of Object.entries(state.servers)) {
if (serverState.enterpriseManaged === true) {
delete rest[url];
}
}
return { servers: rest };
});
},
}),
{
name: "mcp-inspector-oauth",
storage,
},
),
);
}