From 4914a564604524cc188e540d23dfa044292cba73 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Thu, 18 Apr 2024 21:03:36 +0800 Subject: [PATCH] Refactor webhook signature verification process The signature verification process for webhooks has been abstracted. Instead of performing the check directly in the handleWebhook method, it has been moved to a separate service class: PostgresDatabaseWebhookVerifierService. This refactor improves modularity and enables extending or replacing the verification logic more comfortably. The WEBHOOK_SENDER_PROVIDER environment variable is used to determine which verifier service to use. --- apps/web/app/api/db/webhook/route.ts | 12 +-------- packages/database-webhooks/README.md | 18 ++++++++++++- .../database-webhook-handler.service.ts | 15 ++++------- .../database-webhook-verifier.service.ts | 3 +++ .../src/server/services/verifier/index.ts | 19 ++++++++++++++ ...tgres-database-webhook-verifier.service.ts | 25 +++++++++++++++++++ 6 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 packages/database-webhooks/src/server/services/verifier/database-webhook-verifier.service.ts create mode 100644 packages/database-webhooks/src/server/services/verifier/index.ts create mode 100644 packages/database-webhooks/src/server/services/verifier/postgres-database-webhook-verifier.service.ts diff --git a/apps/web/app/api/db/webhook/route.ts b/apps/web/app/api/db/webhook/route.ts index c1ad71794..b0769be1d 100644 --- a/apps/web/app/api/db/webhook/route.ts +++ b/apps/web/app/api/db/webhook/route.ts @@ -1,15 +1,5 @@ -import { z } from 'zod'; - import { DatabaseWebhookHandlerService } from '@kit/database-webhooks'; -const webhooksSecret = z - .string({ - description: `The secret used to verify the webhook signature`, - required_error: `Provide the variable SUPABASE_DB_WEBHOOK_SECRET. This is used to authenticate the webhook event from Supabase.`, - }) - .min(1) - .parse(process.env.SUPABASE_DB_WEBHOOK_SECRET); - const service = new DatabaseWebhookHandlerService(); const response = (status: number) => new Response(null, { status }); @@ -23,7 +13,7 @@ const response = (status: number) => new Response(null, { status }); export async function POST(request: Request) { try { // handle the webhook event - await service.handleWebhook(request, webhooksSecret); + await service.handleWebhook(request); return response(200); } catch { diff --git a/packages/database-webhooks/README.md b/packages/database-webhooks/README.md index bdf32bb5b..44e80113d 100644 --- a/packages/database-webhooks/README.md +++ b/packages/database-webhooks/README.md @@ -5,4 +5,20 @@ This package is responsible for handling webhooks from database changes. For example: 1. when an account is deleted, we handle the cleanup of all related data in the third-party services. 2. when a user is invited, we send an email to the user. -3. when an account member is added, we update the subscription in the third-party services \ No newline at end of file +3. when an account member is added, we update the subscription in the third-party services + +The default sender provider is directly from the Postgres database. + +``` +WEBHOOK_SENDER_PROVIDER=postgres +``` + +Should you add a middleware to the webhook sender provider, you can do so by adding the following to the `WEBHOOK_SENDER_PROVIDER` environment variable. + +``` +WEBHOOK_SENDER_PROVIDER=svix +``` + +For example, you can add [https://docs.svix.com/quickstart]](Swix) as a webhook sender provider that receives webhooks from the database changes and forwards them to your application. + +Svix is not implemented yet. \ No newline at end of file diff --git a/packages/database-webhooks/src/server/services/database-webhook-handler.service.ts b/packages/database-webhooks/src/server/services/database-webhook-handler.service.ts index 7dc104199..609eb70e1 100644 --- a/packages/database-webhooks/src/server/services/database-webhook-handler.service.ts +++ b/packages/database-webhooks/src/server/services/database-webhook-handler.service.ts @@ -5,11 +5,12 @@ import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-clien import { RecordChange, Tables } from '../record-change.type'; import { DatabaseWebhookRouterService } from './database-webhook-router.service'; +import { getDatabaseWebhookVerifier } from './verifier'; export class DatabaseWebhookHandlerService { private readonly namespace = 'database-webhook-handler'; - async handleWebhook(request: Request, webhooksSecret: string) { + async handleWebhook(request: Request) { const logger = await getLogger(); const json = await request.clone().json(); @@ -25,7 +26,9 @@ export class DatabaseWebhookHandlerService { ); // check if the signature is valid - this.assertSignatureIsAuthentic(request, webhooksSecret); + const verifier = await getDatabaseWebhookVerifier(); + + await verifier.verifySignatureOrThrow(request); // all good, handle the webhook @@ -64,12 +67,4 @@ export class DatabaseWebhookHandlerService { throw error; } } - - private assertSignatureIsAuthentic(request: Request, webhooksSecret: string) { - const header = request.headers.get('X-Supabase-Event-Signature'); - - if (header !== webhooksSecret) { - throw new Error('Invalid signature'); - } - } } diff --git a/packages/database-webhooks/src/server/services/verifier/database-webhook-verifier.service.ts b/packages/database-webhooks/src/server/services/verifier/database-webhook-verifier.service.ts new file mode 100644 index 000000000..582206209 --- /dev/null +++ b/packages/database-webhooks/src/server/services/verifier/database-webhook-verifier.service.ts @@ -0,0 +1,3 @@ +export abstract class DatabaseWebhookVerifierService { + abstract verifySignatureOrThrow(request: Request): Promise; +} diff --git a/packages/database-webhooks/src/server/services/verifier/index.ts b/packages/database-webhooks/src/server/services/verifier/index.ts new file mode 100644 index 000000000..730045571 --- /dev/null +++ b/packages/database-webhooks/src/server/services/verifier/index.ts @@ -0,0 +1,19 @@ +const WEBHOOK_SENDER_PROVIDER = + process.env.WEBHOOK_SENDER_PROVIDER ?? 'postgres'; + +export async function getDatabaseWebhookVerifier() { + switch (WEBHOOK_SENDER_PROVIDER) { + case 'postgres': { + const { PostgresDatabaseWebhookVerifierService } = await import( + './postgres-database-webhook-verifier.service' + ); + + return new PostgresDatabaseWebhookVerifierService(); + } + + default: + throw new Error( + `Invalid WEBHOOK_SENDER_PROVIDER: ${WEBHOOK_SENDER_PROVIDER}`, + ); + } +} diff --git a/packages/database-webhooks/src/server/services/verifier/postgres-database-webhook-verifier.service.ts b/packages/database-webhooks/src/server/services/verifier/postgres-database-webhook-verifier.service.ts new file mode 100644 index 000000000..8d1fa7cb9 --- /dev/null +++ b/packages/database-webhooks/src/server/services/verifier/postgres-database-webhook-verifier.service.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import { DatabaseWebhookVerifierService } from './database-webhook-verifier.service'; + +const webhooksSecret = z + .string({ + description: `The secret used to verify the webhook signature`, + required_error: `Provide the variable SUPABASE_DB_WEBHOOK_SECRET. This is used to authenticate the webhook event from Supabase.`, + }) + .min(1) + .parse(process.env.SUPABASE_DB_WEBHOOK_SECRET); + +export class PostgresDatabaseWebhookVerifierService + implements DatabaseWebhookVerifierService +{ + verifySignatureOrThrow(request: Request) { + const header = request.headers.get('X-Supabase-Event-Signature'); + + if (header !== webhooksSecret) { + throw new Error('Invalid signature'); + } + + return Promise.resolve(true); + } +}