annonet/mcpAuthMiddleware.js

97 lines
2.9 KiB
JavaScript

const crypto = require("crypto");
const { verifyOidcAccessToken } = require("./oidcVerifier");
const {
mcpServiceToken,
mcpOrgId,
mcpTeamIds,
mcpAgentName,
mcpOidcClientIds,
mcpRequiredRoles
} = require("../config");
const getBearerToken = (req) => {
const raw = String(req.get("authorization") || "").trim();
if (!raw.toLowerCase().startsWith("bearer ")) return null;
return raw.slice(7).trim() || null;
};
const safeEqual = (left, right) => {
const a = Buffer.from(String(left || ""));
const b = Buffer.from(String(right || ""));
if (a.length === 0 || a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
};
const attachScope = (req, agentName, identity = {}) => {
req.auth = {
userId: `mcp:${agentName}`,
orgId: mcpOrgId,
role: "agent",
isProductAdmin: false,
email: null,
name: agentName,
defaultTeamId: mcpTeamIds[0] || null,
teams: mcpTeamIds.map((id) => ({ id })),
teamIds: [...mcpTeamIds],
actorType: "mcp",
...identity
};
};
const requireMcpAuth = async (req, res, next) => {
try {
const scopeConfigured = Boolean(mcpOrgId && mcpTeamIds.length > 0);
const authConfigured = Boolean(mcpServiceToken || mcpOidcClientIds.length > 0);
if (!scopeConfigured || !authConfigured) {
return res.status(503).json({ error: "MCP bridge is not configured" });
}
const token = getBearerToken(req);
if (!token) {
return res.status(401).json({ error: "MCP bearer token required" });
}
if (mcpServiceToken && safeEqual(token, mcpServiceToken)) {
attachScope(req, mcpAgentName, { identityProvider: "static-token" });
return next();
}
if (mcpOidcClientIds.length === 0) {
return res.status(401).json({ error: "invalid MCP service token" });
}
let verified;
try {
verified = await verifyOidcAccessToken(token, {
allowedClientIds: mcpOidcClientIds,
requiredRoles: mcpRequiredRoles
});
} catch (error) {
return res.status(error.status || 401).json({ error: error.message || "invalid Arrowstack MCP token" });
}
const claims = verified.claims || {};
const clientId = String(verified.clientId || "").trim();
if (!clientId || !mcpOidcClientIds.includes(clientId)) {
return res.status(401).json({ error: "access token authorized party is not an allowed MCP client" });
}
// Authentication proves the Keycloak machine identity. annoNet authorization
// remains explicitly constrained by ANNONET_MCP_ORG_ID, ANNONET_MCP_TEAM_IDS,
// and ANNONET_MCP_AGENT_NAME rather than token-controlled claims.
attachScope(req, mcpAgentName, {
identityProvider: "keycloak",
identitySubject: String(claims.sub || "").trim() || null,
identityClientId: clientId,
identityRoles: verified.roles
});
return next();
} catch (error) {
return next(error);
}
};
module.exports = {
requireMcpAuth
};