Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions apps/mesh/src/api/routes/oauth-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ describe("OAuth Proxy Routes", () => {
expect(res.status).toBe(200);
});

test("returns 502 when origin fetch fails", async () => {
test("returns 404 when origin fetch fails to prevent connection enumeration", async () => {
mockConnectionStorage({
connection_url: "https://origin.example.com/mcp",
});
Expand All @@ -251,9 +251,10 @@ describe("OAuth Proxy Routes", () => {
"/.well-known/oauth-protected-resource/mcp/conn_123",
);

expect(res.status).toBe(502);
// Returns 404 (same as "not found") to avoid leaking connection existence
expect(res.status).toBe(404);
const body = await res.json();
expect(body.error).toBe("Failed to proxy OAuth metadata");
expect(body.error).toBe("Connection not found");
});

test("passes through error responses from origin", async () => {
Expand Down Expand Up @@ -442,6 +443,33 @@ describe("OAuth Proxy Routes", () => {
expect(body.issuer).toBeUndefined();
expect(body.authorization_endpoint).toBeUndefined();
});

test("returns same 404 for non-existent and unreachable connections (no enumeration oracle)", async () => {
// Non-existent connection
mockConnectionStorage(null);
const res404 = await app.request(
"/.well-known/oauth-protected-resource/mcp/conn_nonexistent",
);

// Existing connection with unreachable upstream
mockConnectionStorage({
connection_url: "https://origin.example.com/mcp",
});
global.fetch = mock(() =>
Promise.reject(new Error("Connection refused")),
) as unknown as typeof fetch;
const res502 = await app.request(
"/.well-known/oauth-protected-resource/mcp/conn_existing",
);

// Both should return identical 404 responses
expect(res404.status).toBe(404);
expect(res502.status).toBe(404);
const body404 = await res404.json();
const body502 = await res502.json();
expect(body404.error).toBe("Connection not found");
expect(body502.error).toBe("Connection not found");
});
});

describe("Authorization Server Metadata Proxy", () => {
Expand Down
17 changes: 8 additions & 9 deletions apps/mesh/src/api/routes/oauth-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ async function getConnectionUrl(
connectionId: string,
ctx: MeshContext,
): Promise<string | null> {
const connection = await ctx.storage.connections.findById(connectionId);
const connection = await ctx.storage.connections.findById(
connectionId,
ctx.organization?.id,
);
return connection?.connection_url ?? null;
}

Expand Down Expand Up @@ -467,10 +470,8 @@ const protectedResourceMetadataHandler = async (c: {
"[oauth-proxy] Failed to proxy OAuth protected resource metadata:",
err,
);
return c.json(
{ error: "Failed to proxy OAuth metadata", message: err.message },
502,
);
// Return 404 (same as "not found") to avoid leaking connection existence
return c.json({ error: "Connection not found" }, 404);
}
};

Expand Down Expand Up @@ -623,10 +624,8 @@ app.get(
} catch (error) {
const err = error as Error;
console.error("[oauth-proxy] Failed to proxy auth server metadata:", err);
return c.json(
{ error: "Failed to proxy auth server metadata", message: err.message },
502,
);
// Return 404 (same as "not found") to avoid leaking connection existence
return c.json({ error: "Connection not found or no auth server" }, 404);
}
},
);
Expand Down
Loading