diff --git a/sdk/webhooks.ts b/sdk/webhooks.ts new file mode 100644 index 0000000..3961f49 --- /dev/null +++ b/sdk/webhooks.ts @@ -0,0 +1,27 @@ +import { createHmac, timingSafeEqual } from 'node:crypto'; + +/** + * Generates a signature for a webhook request body using HMAC-SHA256. + * @param requestBody The unmodified request body received by your webhook listener. + * @param sharedSecret The shared secret configured for this specific webhook. + */ +export function generateSignature(requestBody: Buffer, sharedSecret: string): Buffer { + return createHmac('sha256', sharedSecret).update(requestBody).digest(); +} + +/** + * Verifies a webhook's signature to determine if the request was sent by Flagsmith. + * @param requestBody The unmodified request body received by your webhook listener. + * @param receivedSignature The signature received in the webhook's X-Flagsmith-Signature request header. + * @param sharedSecret The shared secret configured for this specific webhook. + * @return True if the signature is valid, false otherwise. + * @throws RangeError receivedSignature is of a different length than the generated signature. + */ +export function verifySignature( + requestBody: Buffer, + receivedSignature: Buffer, + sharedSecret: string +): boolean { + const expectedSignature = generateSignature(requestBody, sharedSecret); + return timingSafeEqual(expectedSignature, receivedSignature); +} diff --git a/tests/sdk/webhooks.test.ts b/tests/sdk/webhooks.test.ts new file mode 100644 index 0000000..cb4aea8 --- /dev/null +++ b/tests/sdk/webhooks.test.ts @@ -0,0 +1,45 @@ +import { generateSignature, verifySignature } from '../../sdk/webhooks.js'; +import { describe, it, expect } from 'vitest'; + +describe('webhooks', () => { + it('test_generate_signature', () => { + // Given + const requestBody = Buffer.from(JSON.stringify({ data: { foo: 123 } })); + const sharedSecret = 'shh'; + + // When + const signature = generateSignature(requestBody, sharedSecret); + + // Then + expect(signature.toString('hex')).toHaveLength(64); // SHA-256 hex digest is 64 characters + }); + + it('test_verify_signature_valid', () => { + // Given + const requestBody = Buffer.from(JSON.stringify({ data: { foo: 123 } })); + const sharedSecret = 'shh'; + + // When + const signature = generateSignature(requestBody, sharedSecret); + + // Then + expect(verifySignature(requestBody, signature, sharedSecret)).toBe(true); + }); + + it('test_verify_signature_invalid', () => { + // Given + const requestBody = Buffer.from( + JSON.stringify({ event: 'flag_updated', data: { id: 123 } }) + ); + + // When + const wrongSignature = generateSignature(Buffer.from('???'), '?'); + + // Then + expect(verifySignature(requestBody, wrongSignature, '?')).toBe(false); + + expect(() => + verifySignature(requestBody, Buffer.from('some invalid signature'), '???') + ).toThrow('Input buffers must have the same byte length'); + }); +});