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).
This commit is contained in:
Bob Dickinson 2026-06-24 19:15:12 -07:00 • committed by GitHub
commit 5de4e911e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -10,6 +10,7 @@ import type {
OAuthTokens,
OAuthMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import type { IdpSessionState } from "./storage.js";
/**
* OAuth state for a single server
@ -21,6 +22,8 @@ export interface ServerOAuthState {
codeVerifier?: string;
scope?: string;
serverMetadata?: OAuthMetadata;
/** Set when resource tokens were minted via EMA (legs 2–3). */
enterpriseManaged?: boolean;
}
/**
@ -28,9 +31,14 @@ export interface ServerOAuthState {
*/
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;
}
/**
@ -47,6 +55,7 @@ export function createOAuthStore(
persist(
(set, get) => ({
servers: {},
idpSessions: {},
getServerState: (serverUrl: string) => {
return get().servers[serverUrl] || {};
},
@ -71,6 +80,38 @@ export function createOAuthStore(
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",