@nvhien20vn/git-mcp-server
Version:
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}.` });
}
}