From d0968a6c9a5e75fc8ac211f2b3de7094898e1c62 Mon Sep 17 00:00:00 2001 From: Anna Hughes Date: Thu, 16 May 2024 17:55:05 +0100 Subject: [PATCH] Fix: storyblok webhook signature (#412) * fix signature * fix: tests for webhooks --------- Co-authored-by: Ellie Re'em --- src/webhooks/webhooks.controller.ts | 6 +++--- src/webhooks/webhooks.service.spec.ts | 20 ++++++++++++++------ src/webhooks/webhooks.service.ts | 8 ++++++-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/webhooks/webhooks.controller.ts b/src/webhooks/webhooks.controller.ts index 67deec95..73ed9647 100644 --- a/src/webhooks/webhooks.controller.ts +++ b/src/webhooks/webhooks.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Headers, Logger, Post, UseGuards } from '@nestjs/common'; +import { Body, Controller, Headers, Logger, Post, Request, UseGuards } from '@nestjs/common'; import { ApiBody, ApiTags } from '@nestjs/swagger'; import { EventLogEntity } from 'src/entities/event-log.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; @@ -46,8 +46,8 @@ export class WebhooksController { @Post('storyblok') @ApiBody({ type: StoryDto }) - async updateStory(@Body() data: StoryDto, @Headers() headers) { + async updateStory(@Request() req, @Body() data: StoryDto, @Headers() headers) { const signature: string | undefined = headers['webhook-signature']; - return this.webhooksService.updateStory(data, signature); + return this.webhooksService.updateStory(req, data, signature); } } diff --git a/src/webhooks/webhooks.service.spec.ts b/src/webhooks/webhooks.service.spec.ts index eaeaf5dc..8930e612 100644 --- a/src/webhooks/webhooks.service.spec.ts +++ b/src/webhooks/webhooks.service.spec.ts @@ -54,8 +54,14 @@ import { WebhooksService } from './webhooks.service'; const webhookSecret = process.env.STORYBLOK_WEBHOOK_SECRET; const getWebhookSignature = (body) => { - return createHmac('sha1', webhookSecret).update(JSON.stringify(body)).digest('hex'); + return createHmac('sha1', webhookSecret).update(""+body).digest('hex'); }; +const createRequestObject = (body) => { + return { + rawBody: "" + body, + setEncoding: ()=>{}, + encoding: "utf8" +}} // Difficult to mock classes as well as node modules. // This seemed the best approach @@ -228,7 +234,7 @@ describe('WebhooksService', () => { text: '', }; - return expect(service.updateStory(body, getWebhookSignature(body))).rejects.toThrow( + return expect(service.updateStory(createRequestObject(body), body, getWebhookSignature(body))).rejects.toThrow( 'STORYBLOK STORY NOT FOUND', ); }); @@ -241,6 +247,7 @@ describe('WebhooksService', () => { }; const deletedStory = (await service.updateStory( + createRequestObject(body), body, getWebhookSignature(body), )) as SessionEntity; @@ -256,6 +263,7 @@ describe('WebhooksService', () => { }; const unpublished = (await service.updateStory( + createRequestObject(body), body, getWebhookSignature(body), )) as SessionEntity; @@ -306,7 +314,7 @@ describe('WebhooksService', () => { text: '', }; - const session = (await service.updateStory(body, getWebhookSignature(body))) as SessionEntity; + const session = (await service.updateStory(createRequestObject(body), body, getWebhookSignature(body))) as SessionEntity; expect(courseFindOneSpy).toHaveBeenCalledWith({ storyblokUuid: 'anotherCourseUuId', @@ -349,7 +357,7 @@ describe('WebhooksService', () => { text: '', }; - const session = (await service.updateStory(body, getWebhookSignature(body))) as SessionEntity; + const session = (await service.updateStory(createRequestObject(body), body, getWebhookSignature(body))) as SessionEntity; expect(session).toEqual(mockSession); expect(courseFindOneSpy).toHaveBeenCalledWith({ @@ -408,7 +416,7 @@ describe('WebhooksService', () => { text: '', }; - const session = (await service.updateStory(body, getWebhookSignature(body))) as SessionEntity; + const session = (await service.updateStory(createRequestObject(body), body, getWebhookSignature(body))) as SessionEntity; expect(session).toEqual(mockSession); expect(sessionSaveRepoSpy).toHaveBeenCalledWith({ @@ -442,7 +450,7 @@ describe('WebhooksService', () => { text: '', }; - const course = (await service.updateStory(body, getWebhookSignature(body))) as CourseEntity; + const course = (await service.updateStory(createRequestObject(body), body, getWebhookSignature(body))) as CourseEntity; expect(course).toEqual(mockCourse); expect(courseFindOneRepoSpy).toHaveBeenCalledWith({ diff --git a/src/webhooks/webhooks.service.ts b/src/webhooks/webhooks.service.ts index a84a8e8f..4573e655 100644 --- a/src/webhooks/webhooks.service.ts +++ b/src/webhooks/webhooks.service.ts @@ -534,7 +534,7 @@ export class WebhooksService { } } - async updateStory(data: StoryDto, signature: string | undefined) { + async updateStory(req, data: StoryDto, signature: string | undefined) { // Verify storyblok signature uses storyblok webhook secret - see https://www.storyblok.com/docs/guide/in-depth/webhooks#securing-a-webhook if (!signature) { const error = `Storyblok webhook error - no signature provided`; @@ -543,7 +543,11 @@ export class WebhooksService { } const webhookSecret = process.env.STORYBLOK_WEBHOOK_SECRET; - const bodyHmac = createHmac('sha1', webhookSecret).update(JSON.stringify(data)).digest('hex'); + + req.rawBody = '' + data; + req.setEncoding('utf8'); + + const bodyHmac = createHmac('sha1', webhookSecret).update(req.rawBody).digest('hex'); if (bodyHmac !== signature) { const error = `Storyblok webhook error - signature mismatch`;