107 lines
3.6 KiB
JavaScript
107 lines
3.6 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import { once } from "node:events";
|
|
import { createServer } from "node:http";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const serverPath = fileURLToPath(new URL("./mock-oauth-mcp-server.mjs", import.meta.url));
|
|
|
|
async function reservePort() {
|
|
const server = createServer();
|
|
server.listen(0, "127.0.0.1");
|
|
await once(server, "listening");
|
|
const address = server.address();
|
|
assert.ok(address && typeof address === "object");
|
|
const closed = once(server, "close");
|
|
server.close();
|
|
await closed;
|
|
return address.port;
|
|
}
|
|
|
|
async function waitFor(check) {
|
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
if (await check()) return;
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
}
|
|
throw new Error("Timed out waiting for mock OAuth MCP server");
|
|
}
|
|
|
|
async function stop(child) {
|
|
if (child.exitCode !== null) return;
|
|
const exited = once(child, "exit");
|
|
child.kill();
|
|
await exited;
|
|
}
|
|
|
|
test("mock OAuth HTML, Basic auth, and errors keep security boundaries", { timeout: 10_000 }, async (context) => {
|
|
const port = await reservePort();
|
|
const origin = `http://127.0.0.1:${port}`;
|
|
const child = spawn(process.execPath, [serverPath], {
|
|
env: {
|
|
...process.env,
|
|
AUTO_APPROVE: "0",
|
|
DISABLE_DCR: "1",
|
|
HOST: "127.0.0.1",
|
|
ISSUER: origin,
|
|
MOCK_CLIENT_ID: "test-client",
|
|
MOCK_CLIENT_SECRET: "test-secret",
|
|
PORT: String(port),
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
context.after(() => stop(child));
|
|
|
|
let stderr = "";
|
|
child.stderr.setEncoding("utf8");
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
await waitFor(async () => {
|
|
try {
|
|
return (await fetch(`${origin}/health`)).ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
const authorizeUrl = new URL(`${origin}/authorize`);
|
|
authorizeUrl.searchParams.set("client_id", "test-client");
|
|
authorizeUrl.searchParams.set("redirect_uri", `${origin}/callback`);
|
|
authorizeUrl.searchParams.set("scope", "mcp:read");
|
|
authorizeUrl.searchParams.set("state", `"><script>alert("unsafe")</script>`);
|
|
const authorizeResponse = await fetch(authorizeUrl);
|
|
const authorizeHtml = await authorizeResponse.text();
|
|
assert.equal(authorizeResponse.status, 200);
|
|
assert.equal(
|
|
authorizeResponse.headers.get("content-security-policy"),
|
|
"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
|
|
);
|
|
const action = authorizeHtml.match(/<form method="post" action="([^"]+)">/)?.[1];
|
|
assert.ok(action);
|
|
assert.match(action, /^\/approve\?/);
|
|
assert.match(action, /&redirect_uri=/);
|
|
assert.doesNotMatch(action, /&redirect_uri=/);
|
|
|
|
const credentials = Buffer.from("test-client:test-secret").toString("base64");
|
|
const tokenResponse = await fetch(`${origin}/token`, {
|
|
method: "POST",
|
|
headers: {
|
|
authorization: `Basic ${" ".repeat(4_096)}${credentials}`,
|
|
"content-type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: "grant_type=refresh_token",
|
|
});
|
|
assert.equal(tokenResponse.status, 200);
|
|
assert.equal(typeof (await tokenResponse.json()).access_token, "string");
|
|
|
|
const failedResponse = await fetch(`${origin}/admin/agent-workloads`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: "{",
|
|
});
|
|
assert.equal(failedResponse.status, 500);
|
|
assert.deepEqual(await failedResponse.json(), { error: "internal_server_error" });
|
|
await waitFor(() => stderr.includes("[mock-oauth-mcp] request failed"));
|
|
});
|