Skip to main content
Glama
zqushair
by zqushair
webhookAuth.ts6.53 kB
import { Request, Response, NextFunction } from 'express'; import crypto from 'crypto'; import { config } from '../config/index.js'; import logger from '../utils/logger.js'; import ErrorLogger from '../utils/errorLogger.js'; // Maximum age of a webhook in milliseconds (5 minutes) const MAX_WEBHOOK_AGE_MS = 5 * 60 * 1000; // Store of processed webhook IDs to prevent replay attacks // In a production environment, this should be stored in a distributed cache or database const processedWebhookIds = new Set<string>(); // Cleanup old webhook IDs periodically (every hour) setInterval(() => { const now = Date.now(); // In a real implementation, we would remove entries older than a certain time // For this simple in-memory implementation, we just clear the set if it gets too large if (processedWebhookIds.size > 10000) { logger.info(`Clearing webhook ID cache (size: ${processedWebhookIds.size})`); processedWebhookIds.clear(); } }, 60 * 60 * 1000); /** * Middleware to verify Frontapp webhook signatures and prevent replay attacks * * Frontapp signs webhooks with a shared secret using HMAC-SHA256 * The signature is sent in the X-Front-Signature header * * This middleware also validates the timestamp in the webhook payload to prevent replay attacks * * @param req Express request object * @param res Express response object * @param next Express next function */ export function verifyWebhookSignature( req: Request, res: Response, next: NextFunction ): void | Response { const signature = req.headers['x-front-signature'] as string; if (!signature) { ErrorLogger.logSecurityError('Missing X-Front-Signature header in webhook request', 'Authentication failed', { path: req.path, method: req.method, ip: req.ip }); return res.status(401).json({ error: 'Missing signature header' }); } // Get the raw request body as a string const rawBody = JSON.stringify(req.body); // Create an HMAC-SHA256 hash using the webhook secret const hmac = crypto.createHmac('sha256', config.webhook.secret); hmac.update(rawBody); const calculatedSignature = hmac.digest('hex'); // Compare the calculated signature with the one from the header if (signature !== calculatedSignature) { ErrorLogger.logSecurityError('Invalid webhook signature', 'Signature verification failed', { expected: calculatedSignature, received: signature, path: req.path, method: req.method, ip: req.ip }); return res.status(401).json({ error: 'Invalid signature' }); } // Extract webhook ID and timestamp from the payload const { payload, type } = req.body; if (!payload) { ErrorLogger.logWebhookError('Missing payload in webhook request', 'Validation failed', { path: req.path, method: req.method, body: req.body }); return res.status(400).json({ error: 'Missing payload' }); } const webhookId = payload.id; if (!webhookId) { ErrorLogger.logWebhookError('Missing ID in webhook payload', 'Validation failed', { path: req.path, method: req.method, type, payload }); return res.status(400).json({ error: 'Missing webhook ID' }); } // Check if this webhook has already been processed (prevent replay) if (processedWebhookIds.has(webhookId)) { ErrorLogger.logSecurityError('Duplicate webhook received', 'Potential replay attack', { webhookId, type, path: req.path, method: req.method, ip: req.ip }); return res.status(409).json({ error: 'Duplicate webhook' }); } // Extract timestamp from the payload (different fields depending on event type) let timestamp: number | undefined; if (payload.created_at) { timestamp = payload.created_at * 1000; // Convert to milliseconds } else if (payload.updated_at) { timestamp = payload.updated_at * 1000; // Convert to milliseconds } else if (payload.timestamp) { timestamp = payload.timestamp * 1000; // Convert to milliseconds } // Validate timestamp if available if (timestamp) { const now = Date.now(); const age = now - timestamp; // Check if the webhook is too old if (age > MAX_WEBHOOK_AGE_MS) { ErrorLogger.logSecurityError('Webhook is too old', 'Potential replay attack', { webhookId, type, timestamp: new Date(timestamp).toISOString(), age: `${age / 1000} seconds`, maxAge: MAX_WEBHOOK_AGE_MS / 1000, path: req.path, method: req.method }); return res.status(400).json({ error: 'Webhook is too old' }); } // Check if the webhook is from the future (clock skew or malicious) if (timestamp > now + 60000) { // Allow 1 minute of clock skew ErrorLogger.logSecurityError('Webhook timestamp is in the future', 'Potential clock skew or malicious request', { webhookId, type, timestamp: new Date(timestamp).toISOString(), now: new Date(now).toISOString(), difference: (timestamp - now) / 1000, path: req.path, method: req.method }); return res.status(400).json({ error: 'Webhook timestamp is in the future' }); } } else { logger.warn('No timestamp found in webhook payload', { webhookId, type, path: req.path, method: req.method }); // Continue processing even without a timestamp, as some webhook types might not include it } // Mark this webhook as processed processedWebhookIds.add(webhookId); // If all checks pass, proceed to the next middleware next(); } /** * Express middleware to capture the raw request body for signature verification * This middleware should be applied before the JSON body parser * * @param req Express request object * @param res Express response object * @param next Express next function */ export function captureRawBody(req: Request, res: Response, next: NextFunction): void { let data = ''; req.on('data', (chunk) => { data += chunk; }); req.on('end', () => { (req as any).rawBody = data; logger.debug('Captured raw request body for webhook signature verification', { path: req.path, method: req.method, contentLength: data.length }); next(); }); req.on('error', (err) => { ErrorLogger.logWebhookError('Error capturing raw request body', err, { path: req.path, method: req.method, contentType: req.headers['content-type'] }); next(err); }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zqushair/Frontapp-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server