🐛 Validate MCP tokens against backend on connect and periodically

This commit is contained in:
Andrey Antukh 2026-07-30 07:00:21 +00:00
parent 25618febcd
commit 54236a466d
2 changed files with 67 additions and 1 deletions

View File

@ -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<typeof setInterval> | 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());

View File

@ -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<boolean> {
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);