@cyanheads/git-mcp-server
Version:
An MCP (Model Context Protocol) server enabling LLMs and AI agents to interact with Git repositories. Provides tools for comprehensive Git operations including clone, commit, branch, diff, log, status, push, pull, merge, rebase, worktree, tag management,
168 lines (167 loc) • 8.41 kB
JavaScript
/**
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
*
* This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
* using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
* It verifies the token's signature and expiration using the secret key defined
* in the configuration (`config.mcpAuthSecretKey`).
*
* If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
* (expected to contain `token`, `clientId`, and `scopes`) is attached to `req.auth`.
* If the token is missing, invalid, or expired, it sends an HTTP 401 Unauthorized response.
*
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
* @module src/mcp-server/transports/authentication/authMiddleware
*/
import jwt from "jsonwebtoken";
import { config, environment } from "../../../config/index.js";
import { logger, requestContextService } from "../../../utils/index.js";
// Startup Validation: Validate secret key presence on module load.
if (environment === "production" && !config.security.mcpAuthSecretKey) {
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment. Authentication cannot proceed securely.");
throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
}
else if (!config.security.mcpAuthSecretKey) {
logger.warning("MCP_AUTH_SECRET_KEY is not set. Authentication middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
}
/**
* Express middleware for verifying JWT Bearer token authentication.
*/
export function mcpAuthMiddleware(req, res, next) {
const context = requestContextService.createRequestContext({
operation: "mcpAuthMiddleware",
method: req.method,
path: req.path,
});
logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
// Development Mode Bypass
if (!config.security.mcpAuthSecretKey) {
if (environment !== "production") {
logger.warning("Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).", context);
// Populate req.auth strictly according to SDK's AuthInfo
req.auth = {
token: "dev-mode-placeholder-token",
clientId: "dev-client-id",
scopes: ["dev-scope"],
};
// Log dev mode details separately, not attaching to req.auth if not part of AuthInfo
logger.debug("Dev mode auth object created.", {
...context,
authDetails: req.auth,
});
return next();
}
else {
logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
res.status(500).json({
error: "Server configuration error: Authentication key missing.",
});
return;
}
}
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
res.status(401).json({
error: "Unauthorized: Missing or invalid authentication token format.",
});
return;
}
const tokenParts = authHeader.split(" ");
if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
logger.warning("Authentication failed: Malformed Bearer token.", context);
res
.status(401)
.json({ error: "Unauthorized: Malformed authentication token." });
return;
}
const rawToken = tokenParts[1];
try {
const decoded = jwt.verify(rawToken, config.security.mcpAuthSecretKey);
if (typeof decoded === "string") {
logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
res
.status(401)
.json({ error: "Unauthorized: Invalid token payload format." });
return;
}
// Extract and validate fields for SDK's AuthInfo
const clientIdFromToken = typeof decoded.cid === "string"
? decoded.cid
: typeof decoded.client_id === "string"
? decoded.client_id
: undefined;
if (!clientIdFromToken) {
logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
res.status(401).json({
error: "Unauthorized: Invalid token, missing client identifier.",
});
return;
}
let scopesFromToken;
if (Array.isArray(decoded.scp) &&
decoded.scp.every((s) => typeof s === "string")) {
scopesFromToken = decoded.scp;
}
else if (typeof decoded.scope === "string" &&
decoded.scope.trim() !== "") {
scopesFromToken = decoded.scope.split(" ").filter((s) => s);
if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
// handles case " " -> [""]
scopesFromToken = [decoded.scope.trim()];
}
else if (scopesFromToken.length === 0 && decoded.scope.trim() === "") {
// If scope is an empty string, treat as no scopes rather than erroring, or use a default.
// Depending on strictness, could also error here. For now, allow empty array if scope was empty string.
logger.debug("JWT 'scope' claim was an empty string, resulting in empty scopes array.", context);
}
}
else {
// If scopes are strictly mandatory and not found or invalid format
logger.warning("Authentication failed: JWT 'scp' or 'scope' claim is missing, not an array of strings, or not a valid space-separated string. Assigning default empty array.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
scopesFromToken = []; // Default to empty array if scopes are mandatory but not found/invalid
// Or, if truly mandatory and must be non-empty:
// res.status(401).json({ error: "Unauthorized: Invalid token, missing or invalid scopes." });
// return;
}
// Construct req.auth with only the properties defined in SDK's AuthInfo
// All other claims from 'decoded' are not part of req.auth for type safety.
req.auth = {
token: rawToken,
clientId: clientIdFromToken,
scopes: scopesFromToken,
};
// Log separately if other JWT claims like 'sub' (sessionId) are needed for app logic
const subClaimForLogging = typeof decoded.sub === "string" ? decoded.sub : undefined;
logger.debug("JWT verified successfully. AuthInfo attached to request.", {
...context,
mcpSessionIdContext: subClaimForLogging,
clientId: req.auth.clientId,
scopes: req.auth.scopes,
});
next();
}
catch (error) {
let errorMessage = "Invalid token";
if (error instanceof jwt.TokenExpiredError) {
errorMessage = "Token expired";
logger.warning("Authentication failed: Token expired.", {
...context,
expiredAt: error.expiredAt,
});
}
else if (error instanceof jwt.JsonWebTokenError) {
errorMessage = `Invalid token: ${error.message}`;
logger.warning(`Authentication failed: ${errorMessage}`, { ...context });
}
else if (error instanceof Error) {
errorMessage = `Verification error: ${error.message}`;
logger.error("Authentication failed: Unexpected error during token verification.", { ...context, error: error.message });
}
else {
errorMessage = "Unknown verification error";
logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
}
res.status(401).json({ error: `Unauthorized: ${errorMessage}.` });
}
}