Skip to content
Merged
8 changes: 4 additions & 4 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Code Scanning
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
schedule:
- cron: '0 0 * * 1'
- cron: "0 0 * * 1"
jobs:
codeql:
permissions:
Expand All @@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'typescript' ]
language: ["typescript"]
steps:
- uses: actions/checkout@v3
with:
Expand Down
54 changes: 31 additions & 23 deletions generated/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"/getting-started/first-steps/cli-quickstart": {
"relPath": "/getting-started/first-steps/cli-quickstart.md",
"lastmod": "2025-05-08T12:32:16.000Z"
"lastmod": "2025-03-27T19:09:28.000Z"
},
"/getting-started/first-steps/existing-cluster": {
"relPath": "/getting-started/first-steps/existing-cluster.md",
Expand Down Expand Up @@ -85,7 +85,7 @@
},
"/getting-started/advanced-config/sandboxing": {
"relPath": "/getting-started/advanced-config/sandboxing.md",
"lastmod": "2025-05-14T21:43:40.000Z"
"lastmod": "2025-03-27T19:09:28.000Z"
},
"/getting-started/advanced-config/network-configuration": {
"relPath": "/getting-started/advanced-config/network-configuration.md",
Expand Down Expand Up @@ -121,15 +121,15 @@
},
"/plural-features/continuous-deployment/observer": {
"relPath": "/plural-features/continuous-deployment/observer.md",
"lastmod": "2025-05-10T04:29:16.000Z"
"lastmod": "2025-05-21T17:00:32.048Z"
},
"/plural-features/continuous-deployment/pipelines": {
"relPath": "/plural-features/continuous-deployment/pipelines.md",
"lastmod": "2025-05-12T06:30:23.000Z"
"lastmod": "2025-05-21T17:00:32.059Z"
},
"/plural-features/k8s-upgrade-assistant": {
"relPath": "/plural-features/k8s-upgrade-assistant/index.md",
"lastmod": "2025-05-11T23:04:59.000Z"
"lastmod": "2025-03-12T14:59:41.000Z"
},
"/plural-features/k8s-upgrade-assistant/upgrade-insights": {
"relPath": "/plural-features/k8s-upgrade-assistant/upgrade-insights.md",
Expand All @@ -141,7 +141,7 @@
},
"/plural-features/k8s-upgrade-assistant/cluster-drain": {
"relPath": "/plural-features/k8s-upgrade-assistant/cluster-drain.md",
"lastmod": "2025-05-13T01:49:39.000Z"
"lastmod": "2025-05-21T17:00:32.101Z"
},
"/plural-features/stacks-iac-management": {
"relPath": "/plural-features/stacks-iac-management/index.md",
Expand Down Expand Up @@ -213,7 +213,7 @@
},
"/plural-features/flows": {
"relPath": "/plural-features/flows/index.md",
"lastmod": "2025-05-11T23:04:59.000Z"
"lastmod": "2025-04-21T22:55:16.000Z"
},
"/plural-features/flows/create-a-flow": {
"relPath": "/plural-features/flows/create-a-flow.md",
Expand All @@ -225,15 +225,23 @@
},
"/plural-features/flows/preview-environments": {
"relPath": "/plural-features/flows/preview-environments.md",
"lastmod": "2025-05-14T21:43:40.000Z"
"lastmod": "2025-05-21T17:00:32.311Z"
},
"/plural-features/flows/mcp": {
"relPath": "/plural-features/flows/mcp.md",
"lastmod": "2025-04-18T18:30:29.000Z"
},
"/plural-features/flows/mcp-auth": {
"relPath": "/plural-features/flows/mcp-auth.md",
"lastmod": "2025-04-22T17:49:06.000Z"
},
"/plural-features/flows/scm-webhooks-and-pr-linking": {
"relPath": "/plural-features/flows/scm-webhooks-and-pr-linking.md",
"lastmod": "2025-05-21T16:56:29.000Z"
},
"/plural-features/observability": {
"relPath": "/plural-features/observability/index.md",
"lastmod": "2025-05-10T04:27:39.000Z"
"lastmod": "2025-04-15T01:53:12.000Z"
},
"/plural-features/observability/prometheus": {
"relPath": "/plural-features/observability/prometheus.md",
Expand All @@ -253,11 +261,11 @@
},
"/plural-features/observability/observability-webhooks/datadog": {
"relPath": "/plural-features/observability/observability-webhooks/datadog.md",
"lastmod": "2025-05-10T04:27:39.000Z"
"lastmod": "2025-05-21T17:00:32.402Z"
},
"/plural-features/observability/observability-webhooks/grafana": {
"relPath": "/plural-features/observability/observability-webhooks/grafana.md",
"lastmod": "2025-05-10T04:27:39.000Z"
"lastmod": "2025-05-21T17:00:32.414Z"
},
"/plural-features/pr-automation": {
"relPath": "/plural-features/pr-automation/index.md",
Expand All @@ -277,7 +285,7 @@
},
"/plural-features/pr-automation/filters": {
"relPath": "/plural-features/pr-automation/filters.md",
"lastmod": "2025-05-16T14:21:39.979Z"
"lastmod": "2025-05-21T17:00:32.465Z"
},
"/plural-features/service-templating": {
"relPath": "/plural-features/service-templating/index.md",
Expand All @@ -289,7 +297,7 @@
},
"/plural-features/projects-and-multi-tenancy": {
"relPath": "/plural-features/projects-and-multi-tenancy/index.md",
"lastmod": "2025-05-15T21:02:36.000Z"
"lastmod": "2025-03-12T14:59:41.000Z"
},
"/plural-features/notifications": {
"relPath": "/plural-features/notifications/index.md",
Expand All @@ -301,23 +309,23 @@
},
"/examples/continuous-deployment": {
"relPath": "/examples/continuous-deployment/index.md",
"lastmod": "2025-05-10T04:28:20.000Z"
"lastmod": "2025-05-21T17:00:32.528Z"
},
"/examples/continuous-deployment/helm-basic-with-inline-values": {
"relPath": "/examples/continuous-deployment/helm-basic-with-inline-values.md",
"lastmod": "2025-05-10T04:28:20.000Z"
"lastmod": "2025-05-21T17:00:32.539Z"
},
"/examples/continuous-deployment/helm-basic-with-values-file": {
"relPath": "/examples/continuous-deployment/helm-basic-with-values-file.md",
"lastmod": "2025-05-10T04:28:20.000Z"
"lastmod": "2025-05-21T17:00:32.551Z"
},
"/examples/continuous-deployment/kustomize-inflate-helm": {
"relPath": "/examples/continuous-deployment/kustomize-inflate-helm.md",
"lastmod": "2025-05-10T04:28:20.000Z"
"lastmod": "2025-05-21T17:00:32.563Z"
},
"/examples/continuous-deployment/kustomize-stack-with-liquid": {
"relPath": "/examples/continuous-deployment/kustomize-stack-with-liquid.md",
"lastmod": "2025-05-10T04:28:20.000Z"
"lastmod": "2025-05-21T17:00:32.575Z"
},
"/faq": {
"relPath": "/faq/index.md",
Expand Down Expand Up @@ -349,7 +357,7 @@
},
"/resources/product-updates": {
"relPath": "/resources/product-updates.md",
"lastmod": "2025-05-01T19:37:01.000Z"
"lastmod": "2025-04-15T19:35:43.000Z"
},
"/getting-started/agent-api-reference": {
"relPath": "/overview/agent-api-reference.md",
Expand Down Expand Up @@ -377,7 +385,7 @@
},
"/deployments/cli-quickstart": {
"relPath": "/getting-started/first-steps/cli-quickstart.md",
"lastmod": "2025-05-08T12:32:16.000Z"
"lastmod": "2025-03-27T19:09:28.000Z"
},
"/deployments/existing-cluster": {
"relPath": "/getting-started/first-steps/existing-cluster.md",
Expand Down Expand Up @@ -425,7 +433,7 @@
},
"/deployments/sandboxing": {
"relPath": "/getting-started/advanced-config/sandboxing.md",
"lastmod": "2025-05-14T21:43:40.000Z"
"lastmod": "2025-03-27T19:09:28.000Z"
},
"/deployments/network-configuration": {
"relPath": "/getting-started/advanced-config/network-configuration.md",
Expand Down Expand Up @@ -457,7 +465,7 @@
},
"/deployments/deprecations": {
"relPath": "/plural-features/k8s-upgrade-assistant/index.md",
"lastmod": "2025-05-11T23:04:59.000Z"
"lastmod": "2025-03-12T14:59:41.000Z"
},
"/stacks/customize-runners": {
"relPath": "/plural-features/stacks-iac-management/customize-runners.md",
Expand Down Expand Up @@ -545,7 +553,7 @@
},
"/deployments/multi-tenancy": {
"relPath": "/plural-features/projects-and-multi-tenancy/index.md",
"lastmod": "2025-05-15T21:02:36.000Z"
"lastmod": "2025-03-12T14:59:41.000Z"
},
"/deployments/notifications": {
"relPath": "/plural-features/notifications/index.md",
Expand Down
145 changes: 145 additions & 0 deletions pages/plural-features/flows/mcp-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
title: MCP Server Authentication and Authorization
description: 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](https://github.com/pluralsh/mcp), 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.

1. **Initialization**: Upon startup, the MCP server (as shown in `mcp/src/index.ts`) calls `initializeJWKS()` (from `mcp/src/auth.ts`).
2. **JWKS Fetching**: The `initializeJWKS` function retrieves the public signing keys from the JWKS URI specified by the `JWKS_URI` environment variable (e.g., `https://your-console-url/.well-known/jwks.json`). It uses the `jwks-rsa` library to fetch and cache the public key. If no signing keys are found, the server fails to start.

```typescript
import 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 };
```

3. **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);
```
4. **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.

## Authorization

Once a token is successfully authenticated, the server performs authorization based on group membership claims within the JWT payload.

1. **Group Claim**: The `authenticateJWT` middleware inspects the decoded JWT payload for a `groups` claim, which should be an array of strings representing the groups the authenticated user belongs to within Plural.
2. **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.
3. **Membership Check**: The middleware verifies if the user's `groups` claim contains at least one of the groups listed in `REQUIRED_GROUPS`.
4. **Access Control**: If the user belongs to at least one required group, the request is allowed to proceed (by calling `next()`). Otherwise, a `401 Unauthorized` response is returned, indicating the user lacks the necessary permissions.

Here is the core `authenticateJWT` middleware function from `mcp/src/auth.ts`:

```typescript
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" });
}
}
```

## Configuration

To enable and configure authentication and authorization in your MCP server based on the `/mcp` example, you need to set the following environment variables:

* `JWT_AUTH_ENABLED`: Set to `"true"` to enable JWT verification.
* `JWKS_URI`: The full URL to your Plural console's JWKS endpoint (e.g., `https://your-console-url/.well-known/jwks.json`).
* `REQUIRED_GROUPS`: A comma-separated string of Plural group names allowed to access the MCP server (e.g., `"sre,devops"`).

By implementing this JWT-based authentication and group-based authorization, you ensure that only authorized users and services within your Plural environment can interact with your custom MCP server, maintaining security for your automated operational tasks.
Loading
Loading