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:
parent
5ee347571f
commit
5de4e911e3
2 changed files with 7585 additions and 595 deletions
|
|
@ -496,6 +496,41 @@ describe("OAuth Store (Zustand)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("NodeOAuthStorage idpSessions (EMA)", () => {
|
||||
let storage: NodeOAuthStorage;
|
||||
const testStatePath = path.join(
|
||||
os.tmpdir(),
|
||||
`mcp-inspector-oauth-idp-${Date.now()}-${Math.random().toString(36).slice(2)}.json`,
|
||||
);
|
||||
const issuer = "https://idp.example.com";
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await fs.unlink(testStatePath);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
|
||||
it("persists IdP session keyed by issuer", async () => {
|
||||
storage = new NodeOAuthStorage(testStatePath);
|
||||
await storage.saveIdpSession(issuer, {
|
||||
idToken: "eyJ.id.token",
|
||||
refreshToken: "rt-1",
|
||||
idTokenExpiresAt: 1_700_000_000_000,
|
||||
});
|
||||
await flushStoreFileWrites(testStatePath);
|
||||
|
||||
const session = await storage.getIdpSession(issuer);
|
||||
expect(session?.idToken).toBe("eyJ.id.token");
|
||||
expect(session?.refreshToken).toBe("rt-1");
|
||||
|
||||
storage.clearIdpSession(issuer);
|
||||
await flushStoreFileWrites(testStatePath);
|
||||
expect(await storage.getIdpSession(issuer)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("NodeOAuthStorage with custom storagePath", () => {
|
||||
const testServerUrl = "http://localhost:3999";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import {
|
|||
OAuthStateMachine,
|
||||
oauthTransitions,
|
||||
} from "@inspector/core/auth/state-machine.js";
|
||||
import type { AuthGuidedState, OAuthStep } from "@inspector/core/auth/types.js";
|
||||
import { EMPTY_GUIDED_STATE } from "@inspector/core/auth/types.js";
|
||||
import type { OAuthFlowState, OAuthStep } from "@inspector/core/auth/types.js";
|
||||
import { EMPTY_OAUTH_FLOW_STATE } from "@inspector/core/auth/types.js";
|
||||
import type { BaseOAuthClientProvider } from "@inspector/core/auth/providers.js";
|
||||
import type {
|
||||
OAuthMetadata,
|
||||
|
|
@ -23,12 +23,12 @@ vi.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
|||
|
||||
describe("OAuthStateMachine", () => {
|
||||
let mockProvider: BaseOAuthClientProvider;
|
||||
let updateState: (updates: Partial<AuthGuidedState>) => void;
|
||||
let state: AuthGuidedState;
|
||||
let updateState: (updates: Partial<OAuthFlowState>) => void;
|
||||
let state: OAuthFlowState;
|
||||
|
||||
beforeEach(() => {
|
||||
state = { ...EMPTY_GUIDED_STATE };
|
||||
updateState = vi.fn((updates: Partial<AuthGuidedState>) => {
|
||||
state = { ...EMPTY_OAUTH_FLOW_STATE };
|
||||
updateState = vi.fn((updates: Partial<OAuthFlowState>) => {
|
||||
state = { ...state, ...updates };
|
||||
});
|
||||
|
||||
|
|
@ -347,8 +347,8 @@ describe("OAuthStateMachine", () => {
|
|||
getServerMetadata: vi.fn(() => metadata),
|
||||
} as unknown as BaseOAuthClientProvider;
|
||||
|
||||
const tokenRequestState: AuthGuidedState = {
|
||||
...EMPTY_GUIDED_STATE,
|
||||
const tokenRequestState: OAuthFlowState = {
|
||||
...EMPTY_OAUTH_FLOW_STATE,
|
||||
oauthStep: "token_request",
|
||||
oauthMetadata: metadata as OAuthMetadata,
|
||||
oauthClientInfo: { client_id: "test-client" },
|
||||
|
|
@ -384,8 +384,8 @@ describe("OAuthStateMachine", () => {
|
|||
clientInformation: vi.fn(async () => undefined),
|
||||
} as unknown as BaseOAuthClientProvider;
|
||||
|
||||
const tokenState: AuthGuidedState = {
|
||||
...EMPTY_GUIDED_STATE,
|
||||
const tokenState: OAuthFlowState = {
|
||||
...EMPTY_OAUTH_FLOW_STATE,
|
||||
oauthStep: "token_request",
|
||||
oauthMetadata: metadata as OAuthMetadata,
|
||||
authorizationCode: "code-without-client",
|
||||
|
|
@ -403,7 +403,7 @@ describe("OAuthStateMachine", () => {
|
|||
|
||||
it("complete.canTransition always returns false (terminal state)", async () => {
|
||||
const result = await oauthTransitions.complete.canTransition({
|
||||
state: { ...EMPTY_GUIDED_STATE, oauthStep: "complete" },
|
||||
state: { ...EMPTY_OAUTH_FLOW_STATE, oauthStep: "complete" },
|
||||
serverUrl: "http://localhost:3000",
|
||||
provider: mockProvider,
|
||||
updateState,
|
||||
|
|
@ -412,7 +412,7 @@ describe("OAuthStateMachine", () => {
|
|||
// execute is a no-op
|
||||
await expect(
|
||||
oauthTransitions.complete.execute({
|
||||
state: { ...EMPTY_GUIDED_STATE, oauthStep: "complete" },
|
||||
state: { ...EMPTY_OAUTH_FLOW_STATE, oauthStep: "complete" },
|
||||
serverUrl: "http://localhost:3000",
|
||||
provider: mockProvider,
|
||||
updateState,
|
||||
|
|
@ -429,8 +429,8 @@ describe("OAuthStateMachine", () => {
|
|||
mockProvider,
|
||||
updateState,
|
||||
);
|
||||
const blockedState: AuthGuidedState = {
|
||||
...EMPTY_GUIDED_STATE,
|
||||
const blockedState: OAuthFlowState = {
|
||||
...EMPTY_OAUTH_FLOW_STATE,
|
||||
oauthStep: "token_request",
|
||||
};
|
||||
await expect(stateMachine.executeStep(blockedState)).rejects.toThrow(
|
||||
|
|
|
|||
Loading…
Reference in a new issue