MCP Server Authentication and Authorization
Understanding how Plural authenticates and authorizes requests to custom MCP servers.
MCP Server Authentication and Authorization
When integrating Plural Flows with a custom MCP server, such as the example provided in our example MCP repository, it's crucial to understand how authentication and authorization are handled to secure your operations. Plural leverages JSON Web Tokens (JWTs) for secure communication between the Plural platform and your MCP server.
Authentication
The authentication mechanism relies on JWTs signed using a standard algorithm. The public key required to verify these tokens is fetched from a JSON Web Key Set (JWKS) endpoint provided by your Plural console instance.
Initialization: Upon startup, the MCP server (as shown in
mcp/src/index.ts
) callsinitializeJWKS()
(frommcp/src/auth.ts
).JWKS Fetching: The
initializeJWKS
function retrieves the public signing keys from the JWKS URI specified by theJWKS_URI
environment variable (e.g.,https://your-console-url/.well-known/jwks.json
). It uses thejwks-rsa
library to fetch and cache the public key. If no signing keys are found, the server fails to start.typescriptimport jwksClient from "jwks-rsa"; let publicKey: string | null = null; async function initializeJWKS() { const JWKS_URI = process.env.JWKS_URI || "https://your-console-url/.well-known/jwks.json"; const client = jwksClient({ jwksUri: JWKS_URI }); const signingKeys = await client.getSigningKeys(); if (signingKeys.length === 0) { throw new Error("No signing keys found in JWKS"); } publicKey = signingKeys[0].getPublicKey(); } export { initializeJWKS };
Middleware: The
authenticateJWT
function acts as Express middleware for the/sse
and/messages
endpoints. This function handles both JWT verification and group-based authorization checks.typescript// import express and MCP servers import { authenticateJWT, initializeJWKS } from "./auth.js"; await initializeJWKS(); // setup MCP server, prompts, tools, etc const app = express(); const transports: { [sessionId: string]: SSEServerTransport } = {}; app.get("/sse", authenticateJWT, async (_: Request, res: Response) => { try { const transport = new SSEServerTransport('/messages', res); transports[transport.sessionId] = transport; res.on("close", () => { delete transports[transport.sessionId]; }); console.error("Starting MCP server.connect with session:", transport.sessionId); await server.connect(transport); console.error("MCP connection complete for session:", transport.sessionId); } catch (err) { console.error("Error during server.connect:", err); res.status(500).send("Internal server error"); } }); app.post("/messages", authenticateJWT, async (req: Request, res: Response) => { const sessionId = req.query.sessionId as string; const transport = transports[sessionId]; if (transport) { await transport.handlePostMessage(req, res); } else { res.status(400).send('No transport found for sessionId'); } }); console.error("Creating MCP Server on port 3000") app.listen(3000);
JWT Verification:
- It checks if JWT authentication is enabled via the
JWT_AUTH_ENABLED
environment variable. If not enabled, it skips authentication. - It extracts the Bearer token from the
Authorization
header. - It verifies the token's signature using the fetched public key (
jsonwebtoken
library). - If the token is missing, malformed, invalid, or expired, it returns a
401 Unauthorized
response.
- It checks if JWT authentication is enabled via the
Authorization
Once a token is successfully authenticated, the server performs authorization based on group membership claims within the JWT payload.
- Group Claim: The
authenticateJWT
middleware inspects the decoded JWT payload for agroups
claim, which should be an array of strings representing the groups the authenticated user belongs to within Plural. - Required Groups: The server checks the
REQUIRED_GROUPS
environment variable. This variable should contain a comma-separated list of Plural group names that are authorized to interact with this specific MCP server. - Membership Check: The middleware verifies if the user's
groups
claim contains at least one of the groups listed inREQUIRED_GROUPS
. - Access Control: If the user belongs to at least one required group, the request is allowed to proceed (by calling
next()
). Otherwise, a401 Unauthorized
response is returned, indicating the user lacks the necessary permissions.
Here is the core authenticateJWT
middleware function from mcp/src/auth.ts
:
import jwtPkg from "jsonwebtoken"; import type { Request, Response, NextFunction } from "express"; // Assumes publicKey has been initialized by initializeJWKS() export function authenticateJWT(req: Request, res: Response, next: NextFunction) { const JWT_AUTH_ENABLED = process.env.JWT_AUTH_ENABLED === "true"; const REQUIRED_GROUPS = process.env.REQUIRED_GROUPS?.split(",") ?? []; if (!JWT_AUTH_ENABLED) return next(); if (!publicKey) return res.status(500).json({ message: "Server not initialized (JWKS public key missing)" }); const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ message: "Missing or malformed token" }); } const token = authHeader.split(" ")[1]; try { const decoded = jwtPkg.verify(token, publicKey); const groups = (decoded as any).groups; if (!Array.isArray(groups)) { return res.status(401).json({ message: "Missing 'groups' claim in token" }); } // Check if user belongs to any required group if (REQUIRED_GROUPS.length > 0 && !REQUIRED_GROUPS.some(g => groups.includes(g))) { return res.status(401).json({ message: "User does not belong to any required group" }); } (req as any).user = decoded; next(); // Authentication and Authorization successful } catch (err) { return res.status(401).json({ message: "Invalid or expired token" }); } }