{"flow":{"id":67,"summary":"HTTP route flow with signature verification template","versions":[245,246,247,248,249,250,273],"created_by":"dieriba.pro916","created_at":"2025-03-23T11:23:34.787Z","votes":0,"approved":true,"apps":["windmill"],"value":{"modules":[],"preprocessor_module":{"id":"preprocessor","value":{"tag":"","type":"rawscript","content":"import * as crypto from 'crypto';\nimport * as wmill from \"windmill-client\";\n\nconst SECRET_KEY_VARIABLE_PATH = \"secret_key_path\";\n\n/**\n * Trigger Preprocessor\n *\n * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.).\n * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication\n * before the 'main' logic is executed in your flow.\n *\n * In this example:\n * - The trigger kind is `http`, which means we have access to HTTP-specific metadata.\n * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as:\n *   - `x-signature` for verifying the integrity and authenticity of the request.\n *   - `x-timestamp` to guard against replay attacks by validating the request time.\n * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature.\n *\n * ⚠️ **Important:** `raw_string` is required for signature verification in this example. \n * Make sure the **\"raw body\"** option is enabled in your HTTP route configuration. \n * If it's not enabled, `raw_string` will be undefined and the script will throw an error.\n *\n * Once the signature and timestamp are verified, we parse the raw JSON body and return the parsed payload as `body`,\n * which is passed as input to the flow's next step.\n *\n * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors\n */\nexport async function preprocessor(\n  event: {\n    kind: 'http';\n    body: any;\n    raw_string: string | null;\n    route: string;\n    path: string;\n    method: string;\n    params: Record<string, string>;\n    query: Record<string, string>;\n    headers: Record<string, string>;\n  },\n) {\n  if (event.kind === 'http') {\n\n    if (!event.raw_string) {\n      throw new Error(\"Missing raw body. Ensure the 'raw body' option is enabled in the HTTP route configuration.\");\n    }\n\n    // Extract signature from headers\n    const signature = event.headers['x-signature'] || event.headers['signature'];\n    if (!signature) {\n      throw new Error('Missing signature in request headers.');\n    }\n\n    // Check timestamp if present to prevent replay attacks\n    const timestamp = event.headers['x-timestamp'] || event.headers['timestamp'];\n    if (timestamp) {\n      const timestampValue = parseInt(timestamp, 10);\n      const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds\n      const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes\n\n      if (isNaN(timestampValue)) {\n        throw new Error('Invalid timestamp format.');\n      }\n\n      if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {\n        throw new Error('Request timestamp is outside the acceptable time window.');\n      }\n    }\n\n    // Verify the signature\n    const isValidSignature = await verifySignature(signature, event.raw_string, timestamp);\n    if (!isValidSignature) {\n      throw new Error('Invalid signature.');\n    }\n\n    // Parse the body if it's JSON (with error handling)\n    let parsedBody: any = {};\n    try {\n      parsedBody = JSON.parse(event.raw_string);\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';\n      throw new Error(`Failed to parse request body: ${errorMessage}`);\n    }\n\n    // Return the parsed body as input to the next step\n    return {\n      body: parsedBody\n    };\n  }\n\n  throw new Error(`Expected trigger of kind 'http', but received: ${event.kind}`);\n}\n\n/**\n * Verifies the HMAC-SHA256 signature against the raw body and optional timestamp.\n *\n * @param signature - The signature from request headers\n * @param body - Raw request body as a string\n * @param timestamp - Optional timestamp from request headers\n * @returns boolean indicating if the signature is valid\n */\nasync function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {\n  const dataToVerify = timestamp\n    ? `${body || ''}${timestamp}`\n    : (body || '');\n\n  const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);\n\n  const expectedSignature = crypto\n    .createHmac('sha256', secretKey || '')\n    .update(dataToVerify)\n    .digest('hex');\n\n  try {\n    return crypto.timingSafeEqual(\n      Buffer.from(signature),\n      Buffer.from(expectedSignature)\n    );\n  } catch (error: unknown) {\n    console.error('Signature comparison error:', error);\n    return false;\n  }\n\n  // NOTE: Modify this logic if your provider uses a different signing mechanism (e.g., Base64, RSA, etc.)\n}","language":"bun","input_transforms":{"event":{"type":"static"}}}}},"schema":{"type":"object","$schema":"https://json-schema.org/draft/2020-12/schema","required":[],"properties":{"body":{"type":"object","description":"The parsed JSON payload from the request, passed into the next step of the flow"}}},"description":"","recording":null,"vcreated_at":"2025-05-16T08:24:43.032Z","vcreated_by":"hugo697","comments":[]}}