From 54236a466d584fba3b3f9b627c96e274ac8f7876 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 30 Jul 2026 07:00:21 +0000 Subject: [PATCH] :bug: Validate MCP tokens against backend on connect and periodically --- mcp/packages/server/src/PenpotMcpServer.ts | 2 + mcp/packages/server/src/PluginBridge.ts | 66 +++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/mcp/packages/server/src/PenpotMcpServer.ts b/mcp/packages/server/src/PenpotMcpServer.ts index 8a4ee30f25..0a047738cf 100644 --- a/mcp/packages/server/src/PenpotMcpServer.ts +++ b/mcp/packages/server/src/PenpotMcpServer.ts @@ -103,6 +103,7 @@ export class PenpotMcpServer { public readonly port: number; public readonly webSocketPort: number; public readonly replPort: number; + public readonly penpotApiUrl: string; private sessionTimeoutInterval: ReturnType | undefined; /** @@ -126,6 +127,7 @@ export class PenpotMcpServer { this.port = parseInt(process.env.PENPOT_MCP_SERVER_PORT ?? "4401", 10); this.webSocketPort = parseInt(process.env.PENPOT_MCP_WEBSOCKET_PORT ?? "4402", 10); this.replPort = parseInt(process.env.PENPOT_MCP_REPL_PORT ?? "4403", 10); + this.penpotApiUrl = process.env.PENPOT_API_URL ?? "http://localhost:6060"; this.tenant = process.env.PENPOT_TENANT ?? "default"; this.configLoader = new ConfigurationLoader(process.cwd()); diff --git a/mcp/packages/server/src/PluginBridge.ts b/mcp/packages/server/src/PluginBridge.ts index 1c24547b8f..f0573320db 100644 --- a/mcp/packages/server/src/PluginBridge.ts +++ b/mcp/packages/server/src/PluginBridge.ts @@ -8,11 +8,14 @@ import type { PenpotMcpServer } from "./PenpotMcpServer"; import type { RedisBridge } from "./RedisBridge"; const KEEP_ALIVE_TIME = 30000; // 30 seconds +const TOKEN_REVALIDATION_INTERVAL = 300000; // 5 minutes +const TOKEN_VALIDATION_TIMEOUT = 5000; // 5 seconds interface ClientConnection { socket: WebSocket; userToken: string | null; pingInterval: NodeJS.Timeout; + revalidationInterval?: NodeJS.Timeout; } /** @@ -48,6 +51,39 @@ export class PluginBridge { this.setupWebSocketHandlers(); } + /** + * Validates a user token against the Penpot backend. + * + * Makes an HTTP request to the backend's get-profile endpoint to verify + * that the token is valid, not expired, and not revoked. + * + * @param userToken - The token to validate + * @returns Promise resolving to true if valid, false otherwise + */ + private async validateUserToken(userToken: string): Promise { + const apiUrl = this.mcpServer.penpotApiUrl; + const url = `${apiUrl}/api/rpc/command/get-profile`; + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), TOKEN_VALIDATION_TIMEOUT); + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Token ${userToken}`, + }, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + return response.ok; + } catch (error) { + this.logger.error(error, "Token validation failed"); + return false; + } + } + /** * Sets up WebSocket connection handlers for plugin communication. * @@ -55,7 +91,7 @@ export class PluginBridge { * channel between the MCP mcpServer and Penpot plugin instances. */ private setupWebSocketHandlers(): void { - this.wsServer.on("connection", (ws: WebSocket, request: http.IncomingMessage) => { + this.wsServer.on("connection", async (ws: WebSocket, request: http.IncomingMessage) => { // extract userToken from query parameters const url = new URL(request.url!, `ws://${request.headers.host}`); const userToken = url.searchParams.get("userToken"); @@ -67,6 +103,17 @@ export class PluginBridge { return; } + // validate token against backend in multi-user mode + if (this.mcpServer.isMultiUserMode() && userToken) { + const isValid = await this.validateUserToken(userToken); + if (!isValid) { + this.logger.warn("Connection attempt with invalid or revoked token - rejecting"); + ws.close(1008, "Invalid or revoked token"); + return; + } + this.logger.info("Token validated successfully"); + } + if (userToken) { this.logger.info("New WebSocket connection established (token provided)"); } else { @@ -80,6 +127,20 @@ export class PluginBridge { // register the client connection with both indexes const connection: ClientConnection = { socket: ws, userToken, pingInterval }; + + // start periodic token revalidation in multi-user mode + if (this.mcpServer.isMultiUserMode() && userToken) { + const revalidationInterval = setInterval(async () => { + const isValid = await this.validateUserToken(userToken); + if (!isValid) { + this.logger.warn("Token revalidation failed - closing connection"); + this.removeConnection(ws); + ws.close(1008, "Token revoked"); + } + }, TOKEN_REVALIDATION_INTERVAL); + connection.revalidationInterval = revalidationInterval; + } + this.connectedClients.set(ws, connection); if (userToken) { // ensure only one connection per userToken @@ -143,6 +204,9 @@ export class PluginBridge { return; } clearInterval(connection.pingInterval); + if (connection.revalidationInterval) { + clearInterval(connection.revalidationInterval); + } this.connectedClients.delete(ws); if (connection.userToken) { this.clientsByToken.delete(connection.userToken);