From d074281280f64ede05a6dec9357cc7c3446f7645 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Sun, 10 Aug 2025 06:45:40 +0530 Subject: [PATCH] fix and enable composio webhook verification --- .../rowboat/app/api/composio/webhook/route.ts | 2 +- apps/rowboat/package-lock.json | 23 ---------- apps/rowboat/package.json | 1 - ...andle-composio-webhook-request.use-case.ts | 46 +++++++++++++++---- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/apps/rowboat/app/api/composio/webhook/route.ts b/apps/rowboat/app/api/composio/webhook/route.ts index 1adf8c55..0f41892b 100644 --- a/apps/rowboat/app/api/composio/webhook/route.ts +++ b/apps/rowboat/app/api/composio/webhook/route.ts @@ -10,7 +10,7 @@ export async function POST(request: Request) { const logger = new PrefixLogger(`composio-webhook-[${id}]`); const payload = await request.text(); const headers = Object.fromEntries(request.headers.entries()); - logger.log('received event', JSON.stringify(headers)); + logger.log('received event', JSON.stringify(headers), payload); // handle webhook try { diff --git a/apps/rowboat/package-lock.json b/apps/rowboat/package-lock.json index 465e2391..e130c3b5 100644 --- a/apps/rowboat/package-lock.json +++ b/apps/rowboat/package-lock.json @@ -60,7 +60,6 @@ "remark-gfm": "^4.0.1", "rowboat-shared": "github:rowboatlabs/shared", "sharp": "^0.33.4", - "standardwebhooks": "^1.0.0", "styled-components": "^5.3.11", "swr": "^2.2.5", "tailwind-merge": "^2.5.5", @@ -7267,12 +7266,6 @@ "node": ">=18.0.0" } }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, "node_modules/@styled-system/background": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz", @@ -11398,12 +11391,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -16859,16 +16846,6 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT" }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/apps/rowboat/package.json b/apps/rowboat/package.json index 170e6919..af44d148 100644 --- a/apps/rowboat/package.json +++ b/apps/rowboat/package.json @@ -68,7 +68,6 @@ "remark-gfm": "^4.0.1", "rowboat-shared": "github:rowboatlabs/shared", "sharp": "^0.33.4", - "standardwebhooks": "^1.0.0", "styled-components": "^5.3.11", "swr": "^2.2.5", "tailwind-merge": "^2.5.5", diff --git a/apps/rowboat/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case.ts b/apps/rowboat/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case.ts index 6c8fb8ef..25d13c9d 100644 --- a/apps/rowboat/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case.ts +++ b/apps/rowboat/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case.ts @@ -1,6 +1,6 @@ import { IJobsRepository } from "@/src/application/repositories/jobs.repository.interface"; import { IComposioTriggerDeploymentsRepository } from "@/src/application/repositories/composio-trigger-deployments.repository.interface"; -import { Webhook } from "standardwebhooks"; +import { createHmac, timingSafeEqual } from "crypto"; import { z } from "zod"; import { BadRequestError } from "@/src/entities/errors/common"; import { UserMessage } from "@/app/lib/types/types"; @@ -52,7 +52,7 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook private readonly jobsRepository: IJobsRepository; private readonly projectsRepository: IProjectsRepository; private readonly pubSubService: IPubSubService; - private webhook; + // no external webhook verifier; using HMAC-SHA256 verification constructor({ composioTriggerDeploymentsRepository, @@ -69,18 +69,17 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook this.jobsRepository = jobsRepository; this.projectsRepository = projectsRepository; this.pubSubService = pubSubService; - this.webhook = new Webhook(WEBHOOK_SECRET); } async execute(request: z.infer): Promise { const { headers, payload } = request; // verify payload - // try { - // this.webhook.verify(payload, headers); - // } catch (error) { - // throw new BadRequestError("Payload verification failed"); - // } + try { + this.verifySignature(headers, payload); + } catch (error) { + throw new BadRequestError("Payload verification failed"); + } // parse event let event: z.infer; @@ -148,4 +147,35 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook logger.log(`Created ${jobs} jobs for trigger ${event.data.trigger_nano_id}`); } + + private verifySignature(headers: Record, payload: string): void { + const normalizedHeaders = Object.fromEntries( + Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]) + ) as Record; + + const webhookId = normalizedHeaders["webhook-id"]; + const webhookTimestamp = normalizedHeaders["webhook-timestamp"]; + const webhookSignature = normalizedHeaders["webhook-signature"]; + + if (!webhookId || !webhookTimestamp || !webhookSignature) { + throw new BadRequestError("Missing required webhook headers"); + } + + const toSign = `${webhookId}.${webhookTimestamp}.${payload}`; + const expectedSignature = createHmac("sha256", WEBHOOK_SECRET) + .update(toSign) + .digest("base64"); + const expectedFullSignature = `v1,${expectedSignature}`; + + const encoder = new TextEncoder(); + const expectedBytes = encoder.encode(expectedFullSignature); + const actualBytes = encoder.encode(webhookSignature); + + const isValid = + expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes); + + if (!isValid) { + throw new BadRequestError("Invalid webhook signature"); + } + } }