Files
n8n/packages/nodes-base/utils/__tests__/webhook-signature-verification.test.ts
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

269 lines
7.7 KiB
TypeScript

import { timingSafeEqual } from 'crypto';
import { verifySignature } from '../webhook-signature-verification';
jest.mock('crypto', () => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
...jest.requireActual('crypto'),
timingSafeEqual: jest.fn(),
}));
describe('webhook-signature-verification', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('verifySignature', () => {
it('should return true when signatures match', () => {
const expectedSignature = 'sha256=abc123';
const actualSignature = 'sha256=abc123';
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature({
getExpectedSignature: () => expectedSignature,
getActualSignature: () => actualSignature,
});
expect(result).toBe(true);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should return false when signatures do not match', () => {
const expectedSignature = 'sha256=abc123';
const actualSignature = 'sha256=xyz789';
(timingSafeEqual as jest.Mock).mockReturnValue(false);
const result = verifySignature({
getExpectedSignature: () => expectedSignature,
getActualSignature: () => actualSignature,
});
expect(result).toBe(false);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should verify signature even when timestamp is skipped', () => {
const expectedSignature = 'sha256=abc123';
const actualSignature = 'sha256=xyz789';
(timingSafeEqual as jest.Mock).mockReturnValue(false);
const result = verifySignature({
getExpectedSignature: () => expectedSignature,
getActualSignature: () => actualSignature,
getTimestamp: () => null,
skipIfNoTimestamp: true,
});
expect(result).toBe(false);
expect(timingSafeEqual).toHaveBeenCalled();
});
it('should return false when signatures have different lengths', () => {
const expectedSignature = 'sha256=abc123';
const actualSignature = 'sha256=abc1234';
const result = verifySignature({
getExpectedSignature: () => expectedSignature,
getActualSignature: () => actualSignature,
});
expect(result).toBe(false);
expect(timingSafeEqual).not.toHaveBeenCalled();
});
it('should return false when expected signature is missing', () => {
const result = verifySignature({
getExpectedSignature: () => null,
getActualSignature: () => 'sha256=abc123',
});
expect(result).toBe(false);
});
it('should return false when expected signature is undefined', () => {
const result = verifySignature({
getExpectedSignature: () => undefined as unknown as string,
getActualSignature: () => 'sha256=abc123',
});
expect(result).toBe(false);
});
it('should return false when actual signature is missing', () => {
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => null,
});
expect(result).toBe(false);
});
it('should return false when actual signature is undefined', () => {
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => undefined as unknown as string,
});
expect(result).toBe(false);
});
it('should return true when skipIfNoExpectedSignature is true and no expected signature', () => {
const result = verifySignature({
getExpectedSignature: () => null,
getActualSignature: () => 'sha256=abc123',
skipIfNoExpectedSignature: true,
});
expect(result).toBe(true);
});
it('should validate timestamp when provided and within window', () => {
const currentTimeSec = Math.floor(Date.now() / 1000);
const recentTimestamp = currentTimeSec - 60; // 1 minute ago
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => recentTimestamp,
});
expect(result).toBe(true);
});
it('should return false when timestamp is too old', () => {
const currentTimeSec = Math.floor(Date.now() / 1000);
const oldTimestamp = currentTimeSec - 400; // More than 5 minutes ago
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => oldTimestamp,
});
expect(result).toBe(false);
});
it('should return false when timestamp is too far in future', () => {
const currentTimeSec = Math.floor(Date.now() / 1000);
const futureTimestamp = currentTimeSec + 400; // More than 5 minutes in future
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => futureTimestamp,
});
expect(result).toBe(false);
});
it('should handle timestamp as string', () => {
const currentTimeSec = Math.floor(Date.now() / 1000);
const recentTimestamp = String(currentTimeSec - 60);
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => recentTimestamp,
});
expect(result).toBe(true);
});
it('should convert milliseconds timestamp to seconds', () => {
const currentTimeMs = Date.now();
const recentTimestampMs = currentTimeMs - 60 * 1000; // 1 minute ago in ms
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => recentTimestampMs,
});
expect(result).toBe(true);
});
it('should use custom maxTimestampAgeSeconds', () => {
const currentTimeSec = Math.floor(Date.now() / 1000);
const timestamp = currentTimeSec - 120; // 2 minutes ago
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => timestamp,
maxTimestampAgeSeconds: 60, // 1 minute window
});
expect(result).toBe(false);
});
it('should return true when skipIfNoTimestamp is true and timestamp is null', () => {
(timingSafeEqual as jest.Mock).mockReturnValue(true);
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => null,
skipIfNoTimestamp: true,
});
expect(result).toBe(true);
});
it('should return false when timestamp is null and skipIfNoTimestamp is false', () => {
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => null,
skipIfNoTimestamp: false,
});
expect(result).toBe(false);
});
it('should return false when timestamp is invalid (NaN)', () => {
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
getTimestamp: () => 'invalid-timestamp',
});
expect(result).toBe(false);
});
it('should handle errors gracefully and return false', () => {
const result = verifySignature({
getExpectedSignature: () => {
throw new Error('Test error');
},
getActualSignature: () => 'sha256=abc123',
});
expect(result).toBe(false);
});
it('should handle timingSafeEqual errors gracefully', () => {
(timingSafeEqual as jest.Mock).mockImplementation(() => {
throw new Error('Buffer length mismatch');
});
const result = verifySignature({
getExpectedSignature: () => 'sha256=abc123',
getActualSignature: () => 'sha256=abc123',
});
expect(result).toBe(false);
});
});
});