Skip to content

Commit 2a2addf

Browse files
Add documentation for establishing a SCM webhook and linking PRs. Also documentation for MCP-Auth (#447)
* mcp auth/authz doc * link example mcp repo and paste code * fix pic path * add mcp-auth to nav bar * updated route.json * updated text and routes * update workflow * more detail
1 parent 6c94c75 commit 2a2addf

File tree

5 files changed

+249
-27
lines changed

5 files changed

+249
-27
lines changed

.github/workflows/codeql.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
name: Code Scanning
22
on:
33
push:
4-
branches: [ main ]
4+
branches: [main]
55
pull_request:
6-
branches: [ main ]
6+
branches: [main]
77
schedule:
8-
- cron: '0 0 * * 1'
8+
- cron: "0 0 * * 1"
99
jobs:
1010
codeql:
1111
permissions:
@@ -17,7 +17,7 @@ jobs:
1717
strategy:
1818
fail-fast: false
1919
matrix:
20-
language: [ 'typescript' ]
20+
language: ["typescript"]
2121
steps:
2222
- uses: actions/checkout@v3
2323
with:

generated/routes.json

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
"/getting-started/first-steps/cli-quickstart": {
3131
"relPath": "/getting-started/first-steps/cli-quickstart.md",
32-
"lastmod": "2025-05-08T12:32:16.000Z"
32+
"lastmod": "2025-03-27T19:09:28.000Z"
3333
},
3434
"/getting-started/first-steps/existing-cluster": {
3535
"relPath": "/getting-started/first-steps/existing-cluster.md",
@@ -85,7 +85,7 @@
8585
},
8686
"/getting-started/advanced-config/sandboxing": {
8787
"relPath": "/getting-started/advanced-config/sandboxing.md",
88-
"lastmod": "2025-05-14T21:43:40.000Z"
88+
"lastmod": "2025-03-27T19:09:28.000Z"
8989
},
9090
"/getting-started/advanced-config/network-configuration": {
9191
"relPath": "/getting-started/advanced-config/network-configuration.md",
@@ -121,15 +121,15 @@
121121
},
122122
"/plural-features/continuous-deployment/observer": {
123123
"relPath": "/plural-features/continuous-deployment/observer.md",
124-
"lastmod": "2025-05-10T04:29:16.000Z"
124+
"lastmod": "2025-05-21T17:00:32.048Z"
125125
},
126126
"/plural-features/continuous-deployment/pipelines": {
127127
"relPath": "/plural-features/continuous-deployment/pipelines.md",
128-
"lastmod": "2025-05-12T06:30:23.000Z"
128+
"lastmod": "2025-05-21T17:00:32.059Z"
129129
},
130130
"/plural-features/k8s-upgrade-assistant": {
131131
"relPath": "/plural-features/k8s-upgrade-assistant/index.md",
132-
"lastmod": "2025-05-11T23:04:59.000Z"
132+
"lastmod": "2025-03-12T14:59:41.000Z"
133133
},
134134
"/plural-features/k8s-upgrade-assistant/upgrade-insights": {
135135
"relPath": "/plural-features/k8s-upgrade-assistant/upgrade-insights.md",
@@ -141,7 +141,7 @@
141141
},
142142
"/plural-features/k8s-upgrade-assistant/cluster-drain": {
143143
"relPath": "/plural-features/k8s-upgrade-assistant/cluster-drain.md",
144-
"lastmod": "2025-05-13T01:49:39.000Z"
144+
"lastmod": "2025-05-21T17:00:32.101Z"
145145
},
146146
"/plural-features/stacks-iac-management": {
147147
"relPath": "/plural-features/stacks-iac-management/index.md",
@@ -213,7 +213,7 @@
213213
},
214214
"/plural-features/flows": {
215215
"relPath": "/plural-features/flows/index.md",
216-
"lastmod": "2025-05-11T23:04:59.000Z"
216+
"lastmod": "2025-04-21T22:55:16.000Z"
217217
},
218218
"/plural-features/flows/create-a-flow": {
219219
"relPath": "/plural-features/flows/create-a-flow.md",
@@ -225,15 +225,23 @@
225225
},
226226
"/plural-features/flows/preview-environments": {
227227
"relPath": "/plural-features/flows/preview-environments.md",
228-
"lastmod": "2025-05-14T21:43:40.000Z"
228+
"lastmod": "2025-05-21T17:00:32.311Z"
229229
},
230230
"/plural-features/flows/mcp": {
231231
"relPath": "/plural-features/flows/mcp.md",
232232
"lastmod": "2025-04-18T18:30:29.000Z"
233233
},
234+
"/plural-features/flows/mcp-auth": {
235+
"relPath": "/plural-features/flows/mcp-auth.md",
236+
"lastmod": "2025-04-22T17:49:06.000Z"
237+
},
238+
"/plural-features/flows/scm-webhooks-and-pr-linking": {
239+
"relPath": "/plural-features/flows/scm-webhooks-and-pr-linking.md",
240+
"lastmod": "2025-05-21T16:56:29.000Z"
241+
},
234242
"/plural-features/observability": {
235243
"relPath": "/plural-features/observability/index.md",
236-
"lastmod": "2025-05-10T04:27:39.000Z"
244+
"lastmod": "2025-04-15T01:53:12.000Z"
237245
},
238246
"/plural-features/observability/prometheus": {
239247
"relPath": "/plural-features/observability/prometheus.md",
@@ -253,11 +261,11 @@
253261
},
254262
"/plural-features/observability/observability-webhooks/datadog": {
255263
"relPath": "/plural-features/observability/observability-webhooks/datadog.md",
256-
"lastmod": "2025-05-10T04:27:39.000Z"
264+
"lastmod": "2025-05-21T17:00:32.402Z"
257265
},
258266
"/plural-features/observability/observability-webhooks/grafana": {
259267
"relPath": "/plural-features/observability/observability-webhooks/grafana.md",
260-
"lastmod": "2025-05-10T04:27:39.000Z"
268+
"lastmod": "2025-05-21T17:00:32.414Z"
261269
},
262270
"/plural-features/pr-automation": {
263271
"relPath": "/plural-features/pr-automation/index.md",
@@ -277,7 +285,7 @@
277285
},
278286
"/plural-features/pr-automation/filters": {
279287
"relPath": "/plural-features/pr-automation/filters.md",
280-
"lastmod": "2025-05-16T14:21:39.979Z"
288+
"lastmod": "2025-05-21T17:00:32.465Z"
281289
},
282290
"/plural-features/service-templating": {
283291
"relPath": "/plural-features/service-templating/index.md",
@@ -289,7 +297,7 @@
289297
},
290298
"/plural-features/projects-and-multi-tenancy": {
291299
"relPath": "/plural-features/projects-and-multi-tenancy/index.md",
292-
"lastmod": "2025-05-15T21:02:36.000Z"
300+
"lastmod": "2025-03-12T14:59:41.000Z"
293301
},
294302
"/plural-features/notifications": {
295303
"relPath": "/plural-features/notifications/index.md",
@@ -301,23 +309,23 @@
301309
},
302310
"/examples/continuous-deployment": {
303311
"relPath": "/examples/continuous-deployment/index.md",
304-
"lastmod": "2025-05-10T04:28:20.000Z"
312+
"lastmod": "2025-05-21T17:00:32.528Z"
305313
},
306314
"/examples/continuous-deployment/helm-basic-with-inline-values": {
307315
"relPath": "/examples/continuous-deployment/helm-basic-with-inline-values.md",
308-
"lastmod": "2025-05-10T04:28:20.000Z"
316+
"lastmod": "2025-05-21T17:00:32.539Z"
309317
},
310318
"/examples/continuous-deployment/helm-basic-with-values-file": {
311319
"relPath": "/examples/continuous-deployment/helm-basic-with-values-file.md",
312-
"lastmod": "2025-05-10T04:28:20.000Z"
320+
"lastmod": "2025-05-21T17:00:32.551Z"
313321
},
314322
"/examples/continuous-deployment/kustomize-inflate-helm": {
315323
"relPath": "/examples/continuous-deployment/kustomize-inflate-helm.md",
316-
"lastmod": "2025-05-10T04:28:20.000Z"
324+
"lastmod": "2025-05-21T17:00:32.563Z"
317325
},
318326
"/examples/continuous-deployment/kustomize-stack-with-liquid": {
319327
"relPath": "/examples/continuous-deployment/kustomize-stack-with-liquid.md",
320-
"lastmod": "2025-05-10T04:28:20.000Z"
328+
"lastmod": "2025-05-21T17:00:32.575Z"
321329
},
322330
"/faq": {
323331
"relPath": "/faq/index.md",
@@ -349,7 +357,7 @@
349357
},
350358
"/resources/product-updates": {
351359
"relPath": "/resources/product-updates.md",
352-
"lastmod": "2025-05-01T19:37:01.000Z"
360+
"lastmod": "2025-04-15T19:35:43.000Z"
353361
},
354362
"/getting-started/agent-api-reference": {
355363
"relPath": "/overview/agent-api-reference.md",
@@ -377,7 +385,7 @@
377385
},
378386
"/deployments/cli-quickstart": {
379387
"relPath": "/getting-started/first-steps/cli-quickstart.md",
380-
"lastmod": "2025-05-08T12:32:16.000Z"
388+
"lastmod": "2025-03-27T19:09:28.000Z"
381389
},
382390
"/deployments/existing-cluster": {
383391
"relPath": "/getting-started/first-steps/existing-cluster.md",
@@ -425,7 +433,7 @@
425433
},
426434
"/deployments/sandboxing": {
427435
"relPath": "/getting-started/advanced-config/sandboxing.md",
428-
"lastmod": "2025-05-14T21:43:40.000Z"
436+
"lastmod": "2025-03-27T19:09:28.000Z"
429437
},
430438
"/deployments/network-configuration": {
431439
"relPath": "/getting-started/advanced-config/network-configuration.md",
@@ -457,7 +465,7 @@
457465
},
458466
"/deployments/deprecations": {
459467
"relPath": "/plural-features/k8s-upgrade-assistant/index.md",
460-
"lastmod": "2025-05-11T23:04:59.000Z"
468+
"lastmod": "2025-03-12T14:59:41.000Z"
461469
},
462470
"/stacks/customize-runners": {
463471
"relPath": "/plural-features/stacks-iac-management/customize-runners.md",
@@ -545,7 +553,7 @@
545553
},
546554
"/deployments/multi-tenancy": {
547555
"relPath": "/plural-features/projects-and-multi-tenancy/index.md",
548-
"lastmod": "2025-05-15T21:02:36.000Z"
556+
"lastmod": "2025-03-12T14:59:41.000Z"
549557
},
550558
"/deployments/notifications": {
551559
"relPath": "/plural-features/notifications/index.md",
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
title: MCP Server Authentication and Authorization
3+
description: Understanding how Plural authenticates and authorizes requests to custom MCP servers.
4+
---
5+
6+
# MCP Server Authentication and Authorization
7+
8+
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.
9+
10+
## Authentication
11+
12+
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.
13+
14+
1. **Initialization**: Upon startup, the MCP server (as shown in `mcp/src/index.ts`) calls `initializeJWKS()` (from `mcp/src/auth.ts`).
15+
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.
16+
17+
```typescript
18+
import jwksClient from "jwks-rsa";
19+
20+
let publicKey: string | null = null;
21+
22+
async function initializeJWKS() {
23+
const JWKS_URI = process.env.JWKS_URI || "https://your-console-url/.well-known/jwks.json";
24+
const client = jwksClient({ jwksUri: JWKS_URI });
25+
26+
const signingKeys = await client.getSigningKeys();
27+
if (signingKeys.length === 0) {
28+
throw new Error("No signing keys found in JWKS");
29+
}
30+
publicKey = signingKeys[0].getPublicKey();
31+
}
32+
33+
export { initializeJWKS };
34+
```
35+
36+
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.
37+
38+
```typescript
39+
// import express and MCP servers
40+
41+
import { authenticateJWT, initializeJWKS } from "./auth.js";
42+
43+
await initializeJWKS();
44+
45+
// setup MCP server, prompts, tools, etc
46+
47+
const app = express();
48+
49+
const transports: { [sessionId: string]: SSEServerTransport } = {};
50+
51+
app.get("/sse", authenticateJWT, async (_: Request, res: Response) => {
52+
try {
53+
const transport = new SSEServerTransport('/messages', res);
54+
transports[transport.sessionId] = transport;
55+
res.on("close", () => {
56+
delete transports[transport.sessionId];
57+
});
58+
console.error("Starting MCP server.connect with session:", transport.sessionId);
59+
await server.connect(transport);
60+
console.error("MCP connection complete for session:", transport.sessionId);
61+
} catch (err) {
62+
console.error("Error during server.connect:", err);
63+
res.status(500).send("Internal server error");
64+
}
65+
});
66+
67+
app.post("/messages", authenticateJWT, async (req: Request, res: Response) => {
68+
const sessionId = req.query.sessionId as string;
69+
const transport = transports[sessionId];
70+
if (transport) {
71+
await transport.handlePostMessage(req, res);
72+
} else {
73+
res.status(400).send('No transport found for sessionId');
74+
}
75+
});
76+
77+
console.error("Creating MCP Server on port 3000")
78+
app.listen(3000);
79+
```
80+
4. **JWT Verification**:
81+
* It checks if JWT authentication is enabled via the `JWT_AUTH_ENABLED` environment variable. If not enabled, it skips authentication.
82+
* It extracts the Bearer token from the `Authorization` header.
83+
* It verifies the token's signature using the fetched public key (`jsonwebtoken` library).
84+
* If the token is missing, malformed, invalid, or expired, it returns a `401 Unauthorized` response.
85+
86+
## Authorization
87+
88+
Once a token is successfully authenticated, the server performs authorization based on group membership claims within the JWT payload.
89+
90+
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.
91+
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.
92+
3. **Membership Check**: The middleware verifies if the user's `groups` claim contains at least one of the groups listed in `REQUIRED_GROUPS`.
93+
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.
94+
95+
Here is the core `authenticateJWT` middleware function from `mcp/src/auth.ts`:
96+
97+
```typescript
98+
import jwtPkg from "jsonwebtoken";
99+
import type { Request, Response, NextFunction } from "express";
100+
101+
// Assumes publicKey has been initialized by initializeJWKS()
102+
103+
export function authenticateJWT(req: Request, res: Response, next: NextFunction) {
104+
const JWT_AUTH_ENABLED = process.env.JWT_AUTH_ENABLED === "true";
105+
const REQUIRED_GROUPS = process.env.REQUIRED_GROUPS?.split(",") ?? [];
106+
107+
if (!JWT_AUTH_ENABLED) return next();
108+
if (!publicKey) return res.status(500).json({ message: "Server not initialized (JWKS public key missing)" });
109+
110+
const authHeader = req.headers.authorization;
111+
if (!authHeader?.startsWith("Bearer ")) {
112+
return res.status(401).json({ message: "Missing or malformed token" });
113+
}
114+
115+
const token = authHeader.split(" ")[1];
116+
try {
117+
const decoded = jwtPkg.verify(token, publicKey);
118+
const groups = (decoded as any).groups;
119+
120+
if (!Array.isArray(groups)) {
121+
return res.status(401).json({ message: "Missing 'groups' claim in token" });
122+
}
123+
124+
// Check if user belongs to any required group
125+
if (REQUIRED_GROUPS.length > 0 && !REQUIRED_GROUPS.some(g => groups.includes(g))) {
126+
return res.status(401).json({ message: "User does not belong to any required group" });
127+
}
128+
129+
(req as any).user = decoded;
130+
next(); // Authentication and Authorization successful
131+
} catch (err) {
132+
return res.status(401).json({ message: "Invalid or expired token" });
133+
}
134+
}
135+
```
136+
137+
## Configuration
138+
139+
To enable and configure authentication and authorization in your MCP server based on the `/mcp` example, you need to set the following environment variables:
140+
141+
* `JWT_AUTH_ENABLED`: Set to `"true"` to enable JWT verification.
142+
* `JWKS_URI`: The full URL to your Plural console's JWKS endpoint (e.g., `https://your-console-url/.well-known/jwks.json`).
143+
* `REQUIRED_GROUPS`: A comma-separated string of Plural group names allowed to access the MCP server (e.g., `"sre,devops"`).
144+
145+
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.

0 commit comments

Comments
 (0)