fix and enable composio webhook verification

This commit is contained in:
Ramnique Singh 2025-08-10 06:45:40 +05:30
parent 23d88aa7c0
commit d074281280
4 changed files with 39 additions and 33 deletions

View file

@ -10,7 +10,7 @@ export async function POST(request: Request) {
const logger = new PrefixLogger(`composio-webhook-[${id}]`); const logger = new PrefixLogger(`composio-webhook-[${id}]`);
const payload = await request.text(); const payload = await request.text();
const headers = Object.fromEntries(request.headers.entries()); const headers = Object.fromEntries(request.headers.entries());
logger.log('received event', JSON.stringify(headers)); logger.log('received event', JSON.stringify(headers), payload);
// handle webhook // handle webhook
try { try {

View file

@ -60,7 +60,6 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"rowboat-shared": "github:rowboatlabs/shared", "rowboat-shared": "github:rowboatlabs/shared",
"sharp": "^0.33.4", "sharp": "^0.33.4",
"standardwebhooks": "^1.0.0",
"styled-components": "^5.3.11", "styled-components": "^5.3.11",
"swr": "^2.2.5", "swr": "^2.2.5",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
@ -7267,12 +7266,6 @@
"node": ">=18.0.0" "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": { "node_modules/@styled-system/background": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz", "resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz",
@ -11398,12 +11391,6 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true "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": { "node_modules/fast-xml-parser": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
@ -16859,16 +16846,6 @@
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT" "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": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",

View file

@ -68,7 +68,6 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"rowboat-shared": "github:rowboatlabs/shared", "rowboat-shared": "github:rowboatlabs/shared",
"sharp": "^0.33.4", "sharp": "^0.33.4",
"standardwebhooks": "^1.0.0",
"styled-components": "^5.3.11", "styled-components": "^5.3.11",
"swr": "^2.2.5", "swr": "^2.2.5",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",

View file

@ -1,6 +1,6 @@
import { IJobsRepository } from "@/src/application/repositories/jobs.repository.interface"; import { IJobsRepository } from "@/src/application/repositories/jobs.repository.interface";
import { IComposioTriggerDeploymentsRepository } from "@/src/application/repositories/composio-trigger-deployments.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 { z } from "zod";
import { BadRequestError } from "@/src/entities/errors/common"; import { BadRequestError } from "@/src/entities/errors/common";
import { UserMessage } from "@/app/lib/types/types"; import { UserMessage } from "@/app/lib/types/types";
@ -52,7 +52,7 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook
private readonly jobsRepository: IJobsRepository; private readonly jobsRepository: IJobsRepository;
private readonly projectsRepository: IProjectsRepository; private readonly projectsRepository: IProjectsRepository;
private readonly pubSubService: IPubSubService; private readonly pubSubService: IPubSubService;
private webhook; // no external webhook verifier; using HMAC-SHA256 verification
constructor({ constructor({
composioTriggerDeploymentsRepository, composioTriggerDeploymentsRepository,
@ -69,18 +69,17 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook
this.jobsRepository = jobsRepository; this.jobsRepository = jobsRepository;
this.projectsRepository = projectsRepository; this.projectsRepository = projectsRepository;
this.pubSubService = pubSubService; this.pubSubService = pubSubService;
this.webhook = new Webhook(WEBHOOK_SECRET);
} }
async execute(request: z.infer<typeof requestSchema>): Promise<void> { async execute(request: z.infer<typeof requestSchema>): Promise<void> {
const { headers, payload } = request; const { headers, payload } = request;
// verify payload // verify payload
// try { try {
// this.webhook.verify(payload, headers); this.verifySignature(headers, payload);
// } catch (error) { } catch (error) {
// throw new BadRequestError("Payload verification failed"); throw new BadRequestError("Payload verification failed");
// } }
// parse event // parse event
let event: z.infer<typeof payloadSchema>; let event: z.infer<typeof payloadSchema>;
@ -148,4 +147,35 @@ export class HandleCompsioWebhookRequestUseCase implements IHandleCompsioWebhook
logger.log(`Created ${jobs} jobs for trigger ${event.data.trigger_nano_id}`); logger.log(`Created ${jobs} jobs for trigger ${event.data.trigger_nano_id}`);
} }
private verifySignature(headers: Record<string, string>, payload: string): void {
const normalizedHeaders = Object.fromEntries(
Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])
) as Record<string, string>;
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");
}
}
} }