* test: cover EMA interactive leg 1 end-to-end (#1693) The EMA e2e suite only drove silent EMA (seeded IdP session + legs 2–3); the interactive leg-1 authorization-code exchange — the exact path that broke in #1688 (a dropped RFC 9207 iss) — had no e2e coverage. - Extend the mock IdP (ema-mock-servers.ts) with an interactive authorization endpoint (GET /authorize auto-approves and redirects with code + state + RFC 9207 iss) and an authorization_code branch on POST /token (client-cred + PKCE S256 check → id_token). The IdP metadata already advertises authorization_response_iss_parameter_supported, so the SDK enforces iss on the exchange. - Add two leg-1 e2e tests: the full authorize → callback(iss) → id_token → resource-token → connect ladder, and the RFC 9207 guard that a missing callback iss rejects the exchange before any token is stored. Part 1 of #1693 (composable test-server AS advertising + emitting iss, plus standard-OAuth iss coverage) already landed in #1725. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * test: apply review nits — simplify iss regex, dedupe redirect provider Address non-blocking review nits on #1693: - Drop the redundant `Issuer mismatch|` alternation from the guard test's matcher; `/issuer/i` already matches (case-insensitive). - Export `createStaticRedirectUrlProvider` from the shared oauth fixtures helper and reuse it in the EMA e2e, removing the local duplicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
40aaf47705
commit
93cd0041a0
1 changed files with 178 additions and 6 deletions
|
|
@ -30,6 +30,10 @@ import {
|
|||
} from "@modelcontextprotocol/inspector-test-server";
|
||||
import type { InspectorClientOptions } from "@inspector/core/mcp/inspectorClient.js";
|
||||
import type { MCPServerConfig } from "@inspector/core/mcp/types.js";
|
||||
import {
|
||||
completeOAuthAuthorization,
|
||||
createStaticRedirectUrlProvider,
|
||||
} from "../helpers/oauth-client-fixtures.js";
|
||||
import {
|
||||
createEmaMockKeyMaterial,
|
||||
createMockIdToken,
|
||||
|
|
@ -48,10 +52,6 @@ const oauthTestStatePath = path.join(
|
|||
`mcp-ema-${process.pid}-inspectorClient-ema-e2e.json`,
|
||||
);
|
||||
|
||||
function createStaticRedirectUrlProvider(redirectUrl: string) {
|
||||
return { getRedirectUrl: () => redirectUrl };
|
||||
}
|
||||
|
||||
async function waitForProtectedResourceMetadata(
|
||||
serverBase: string,
|
||||
): Promise<void> {
|
||||
|
|
@ -208,4 +208,65 @@ describe("InspectorClient EMA E2E", () => {
|
|||
expect(entry?.tokens?.access_token).toBeDefined();
|
||||
expect(entry?.enterpriseManaged).toBe(true);
|
||||
});
|
||||
|
||||
describe("interactive leg 1 (no cached IdP session)", () => {
|
||||
// The suite-wide beforeEach seeds an IdP session so the silent path (legs
|
||||
// 2–3) runs. These tests clear it first so EMA falls through to the
|
||||
// interactive authorization-code login against the mock IdP — the exact path
|
||||
// that regressed in #1688 (a dropped RFC 9207 `iss`), which had no e2e
|
||||
// coverage before #1693.
|
||||
beforeEach(async () => {
|
||||
await storage.clearIdpSession(mockIdp.baseUrl);
|
||||
await flushStoreFileWrites(oauthTestStatePath);
|
||||
});
|
||||
|
||||
it("drives leg 1 end to end: authorize → callback(iss) → id_token → resource token", async () => {
|
||||
client = createEmaClient();
|
||||
|
||||
// No cached IdP session → EMA starts interactive leg 1 and returns the IdP
|
||||
// authorization URL instead of silently connecting.
|
||||
const authUrl = await client.authenticate();
|
||||
if (!authUrl) throw new Error("Expected IdP authorization URL for leg 1");
|
||||
expect(authUrl.href.startsWith(mockIdp.baseUrl)).toBe(true);
|
||||
expect(authUrl.pathname).toBe("/authorize");
|
||||
|
||||
// The mock IdP auto-approves and redirects back with code + RFC 9207 iss.
|
||||
const { code, iss } = await completeOAuthAuthorization(authUrl);
|
||||
expect(iss).toBe(mockIdp.baseUrl);
|
||||
|
||||
// Leg 1 code exchange (mints the ID Token) → legs 2–3 (resource token).
|
||||
await client.completeOAuthFlow(code, iss);
|
||||
await client.connect();
|
||||
|
||||
expect(client.getStatus()).toBe("connected");
|
||||
|
||||
const tokens = await client.getOAuthTokens();
|
||||
expect(tokens?.access_token).toBeDefined();
|
||||
expect(tokens?.token_type).toBe("Bearer");
|
||||
|
||||
const oauthState = await client.getOAuthState();
|
||||
expect(oauthState?.protocol).toBe("ema");
|
||||
expect(oauthState?.authorized).toBe(true);
|
||||
expect(oauthState?.ema?.idpSession).toBe("logged_in");
|
||||
|
||||
// Leg 1 side effect: the exchanged ID Token is now cached as the IdP session.
|
||||
const session = await storage.getIdpSession(mockIdp.baseUrl);
|
||||
expect(session?.idToken).toBeDefined();
|
||||
});
|
||||
|
||||
it("rejects the leg 1 exchange when the callback iss is missing (RFC 9207)", async () => {
|
||||
client = createEmaClient();
|
||||
|
||||
const authUrl = await client.authenticate();
|
||||
if (!authUrl) throw new Error("Expected IdP authorization URL for leg 1");
|
||||
const { code } = await completeOAuthAuthorization(authUrl);
|
||||
|
||||
// The mock IdP metadata advertises
|
||||
// `authorization_response_iss_parameter_supported`, so the SDK must reject
|
||||
// a code exchange that forwards no `iss`. This is the guard that a dropped
|
||||
// `iss` (the #1688 regression) would trip.
|
||||
await expect(client.completeOAuthFlow(code)).rejects.toThrow(/issuer/i);
|
||||
expect(await client.getOAuthTokens()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue