fix MCP auth token refresh and disable legacy SSE

This commit is contained in:
Jordan Diaz
2026-07-18 10:03:12 +00:00
parent 9c3d9fb999
commit 987e050539
12 changed files with 437 additions and 128 deletions

View File

@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createMcpAuthMiddleware } from "../auth/mcpAuthMiddleware.js";
async function invoke(middleware, headers = {}, path = "/mcp") {
const req = { headers: { ...headers }, path };
const response = {};
const res = {
status(code) { response.status = code; return this; },
setHeader() { return this; },
end(body) { response.body = JSON.parse(body); return this; },
};
let nextCalled = false;
await middleware(req, res, () => { nextCalled = true; });
return { req, response, nextCalled };
}
test("rejects missing and invalid MCP secrets", async () => {
const middleware = createMcpAuthMiddleware({ validateToken: async () => null });
assert.equal((await invoke(middleware)).response.status, 401);
assert.equal((await invoke(middleware, { "x-mcp-secret": "bad" })).response.status, 401);
});
test("replaces spoofed identity and enforces project scope", async () => {
const middleware = createMcpAuthMiddleware({
validateToken: async () => ({ user: "owner", project: "allowed" }),
});
const result = await invoke(middleware, {
"x-mcp-secret": "valid",
"x-acai-user": "attacker",
"x-project-name": "other",
});
assert.equal(result.nextCalled, true);
assert.equal(result.req.headers["x-acai-user"], "owner");
assert.equal(result.req.headers["x-project-name"], "allowed");
});
test("requires project context for user-wide tokens", async () => {
const middleware = createMcpAuthMiddleware({
validateToken: async () => ({ user: "owner", project: "" }),
});
assert.equal((await invoke(middleware, { "x-mcp-secret": "valid" })).response.status, 400);
const accepted = await invoke(middleware, {
"x-mcp-secret": "valid",
"x-project-name": "demo",
});
assert.equal(accepted.nextCalled, true);
});
test("revocation is effective on the next request in an existing session", async () => {
let revoked = false;
const middleware = createMcpAuthMiddleware({
validateToken: async () => revoked ? null : ({ user: "owner", project: "demo" }),
});
const headers = { "x-mcp-secret": "valid", "x-project-name": "demo" };
assert.equal((await invoke(middleware, headers)).nextCalled, true);
revoked = true;
assert.equal((await invoke(middleware, headers)).response.status, 401);
});
test("does not apply MCP auth to legacy SSE paths", async () => {
const middleware = createMcpAuthMiddleware({ validateToken: async () => null });
assert.equal((await invoke(middleware, { "x-acai-user": "legacy" }, "/sse")).nextCalled, true);
});

View File

@@ -0,0 +1,141 @@
import assert from "node:assert/strict";
import test from "node:test";
import { rebuildApiClient, runWithTokenRefreshRetry } from "../auth/apiClient.js";
import { mcpSessionCredentials, sessionApiClients, sessionCredentials } from "../auth/credentials.js";
import { refreshSessionCredentials } from "../auth/sessionRefresh.js";
const jwt = (expiresIn = 3600) => {
const payload = Buffer.from(JSON.stringify({
exp: Math.floor(Date.now() / 1000) + expiresIn,
})).toString("base64url");
return `header.${payload}.signature`;
};
const credentials = (token) => ({
token,
tokenHash: "hash",
website: "demo.example",
web_url: "https://demo.forge.example",
api_web_url: "http://web:80",
forge_host: "demo.forge.example",
mode: "local",
project_dir: "/opt/acai/webs/owner/demo",
acai_user: "owner",
mcp_secret: "acai_secret",
role: "developer",
});
test("deduplicates concurrent forced refreshes and updates session credentials", async () => {
const sessionId = "refresh-session";
sessionCredentials.set(sessionId, credentials(jwt()));
let calls = 0;
const freshToken = jwt(7200);
const fetcher = async () => {
calls += 1;
await new Promise((resolve) => setTimeout(resolve, 10));
return {
success: true,
token: freshToken,
tokenHash: "fresh-hash",
domain: "demo.example",
web_url: "https://demo.forge.example",
api_web_url: "http://web:80",
project_dir: "/opt/acai/webs/owner/demo",
};
};
const [first, second] = await Promise.all([
refreshSessionCredentials(sessionId, { force: true, fetcher }),
refreshSessionCredentials(sessionId, { force: true, fetcher }),
]);
assert.equal(calls, 1);
assert.equal(first.token, freshToken);
assert.equal(second.token, freshToken);
assert.equal(sessionCredentials.get(sessionId).tokenHash, "fresh-hash");
assert.equal(mcpSessionCredentials.get(sessionId).credentials.mcp_secret, undefined);
mcpSessionCredentials.delete(sessionId);
sessionCredentials.delete(sessionId);
});
test("cached Axios client reads the latest token for every request", async () => {
const sessionId = "axios-session";
sessionCredentials.set(sessionId, credentials("old-token"));
const client = await rebuildApiClient(sessionId);
const observed = [];
client.defaults.adapter = async (config) => {
observed.push(config.headers["X-Acai-Token"]);
return { data: {}, status: 200, statusText: "OK", headers: {}, config };
};
await client.get("/first");
sessionCredentials.set(sessionId, credentials("new-token"));
await client.get("/second");
assert.deepEqual(observed, ["old-token", "new-token"]);
sessionApiClients.delete(sessionId);
sessionCredentials.delete(sessionId);
});
test("403 token failure forces one refresh and retries the original request once", async () => {
const sessionId = "retry-session";
sessionCredentials.set(sessionId, credentials("expired-token"));
let refreshes = 0;
let attempts = 0;
const observed = [];
const fresh = credentials("fresh-token");
const client = await rebuildApiClient(sessionId, {
refreshCredentials: async (_sessionId, options) => {
refreshes += 1;
assert.equal(options.force, true);
sessionCredentials.set(sessionId, fresh);
return fresh;
},
});
client.defaults.adapter = async (config) => {
attempts += 1;
observed.push(config.headers["X-Acai-Token"]);
if (attempts === 1) {
const error = new Error("Request failed with status code 403");
error.config = config;
error.response = { status: 403, data: { error: "Token no valido" } };
throw error;
}
return { data: { success: true }, status: 200, statusText: "OK", headers: {}, config };
};
const response = await client.get("/retry-once");
assert.equal(response.data.success, true);
assert.equal(refreshes, 1);
assert.equal(attempts, 2);
assert.deepEqual(observed, ["expired-token", "fresh-token"]);
sessionApiClients.delete(sessionId);
sessionCredentials.delete(sessionId);
});
test("tool-level retry covers Axios clients outside the cached API client", async () => {
let attempts = 0;
let refreshes = 0;
const operation = async () => {
attempts += 1;
if (attempts === 1) {
const error = new Error("forbidden");
error.response = { status: 403, data: { error: "JWT expired" } };
throw error;
}
return "ok";
};
const result = await runWithTokenRefreshRetry("tool-session", operation, {
refreshCredentials: async (_sessionId, options) => {
refreshes += 1;
assert.equal(options.force, true);
},
});
assert.equal(result, "ok");
assert.equal(attempts, 2);
assert.equal(refreshes, 1);
});