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 };