97 lines
2.9 KiB
JavaScript
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
|
|
};
|