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:
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export abstract class DatabaseWebhookVerifierService {
|
||||||
|
abstract verifySignatureOrThrow(request: Request): Promise<boolean>;
|
||||||
|
}
|
||||||
@@ -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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user