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.
This commit is contained in:
giancarlo
2024-04-18 21:03:36 +08:00
parent 98a4c02020
commit 4914a56460
6 changed files with 70 additions and 22 deletions

View File

@@ -1,15 +1,5 @@
import { z } from 'zod';
import { DatabaseWebhookHandlerService } from '@kit/database-webhooks'; 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 service = new DatabaseWebhookHandlerService();
const response = (status: number) => new Response(null, { status }); 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) { export async function POST(request: Request) {
try { try {
// handle the webhook event // handle the webhook event
await service.handleWebhook(request, webhooksSecret); await service.handleWebhook(request);
return response(200); return response(200);
} catch { } catch {

View File

@@ -5,4 +5,20 @@ This package is responsible for handling webhooks from database changes.
For example: For example:
1. when an account is deleted, we handle the cleanup of all related data in the third-party services. 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. 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 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.

View File

@@ -5,11 +5,12 @@ import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-clien
import { RecordChange, Tables } from '../record-change.type'; import { RecordChange, Tables } from '../record-change.type';
import { DatabaseWebhookRouterService } from './database-webhook-router.service'; import { DatabaseWebhookRouterService } from './database-webhook-router.service';
import { getDatabaseWebhookVerifier } from './verifier';
export class DatabaseWebhookHandlerService { export class DatabaseWebhookHandlerService {
private readonly namespace = 'database-webhook-handler'; private readonly namespace = 'database-webhook-handler';
async handleWebhook(request: Request, webhooksSecret: string) { async handleWebhook(request: Request) {
const logger = await getLogger(); const logger = await getLogger();
const json = await request.clone().json(); const json = await request.clone().json();
@@ -25,7 +26,9 @@ export class DatabaseWebhookHandlerService {
); );
// check if the signature is valid // check if the signature is valid
this.assertSignatureIsAuthentic(request, webhooksSecret); const verifier = await getDatabaseWebhookVerifier();
await verifier.verifySignatureOrThrow(request);
// all good, handle the webhook // all good, handle the webhook
@@ -64,12 +67,4 @@ export class DatabaseWebhookHandlerService {
throw error; 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');
}
}
} }

View File

@@ -0,0 +1,3 @@
export abstract class DatabaseWebhookVerifierService {
abstract verifySignatureOrThrow(request: Request): Promise<boolean>;
}

View File

@@ -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}`,
);
}
}

View File

@@ -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);
}
}