mirror of
https://github.com/penpot/penpot.git
synced 2026-08-07 21:38:48 +00:00
🐛 Validate MCP tokens against backend on connect and periodically
This commit is contained in:
parent
25618febcd
commit
54236a466d
@ -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());
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user