-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebhook-signature.ts
More file actions
78 lines (66 loc) · 1.9 KB
/
webhook-signature.ts
File metadata and controls
78 lines (66 loc) · 1.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import { createHmac, timingSafeEqual } from 'node:crypto';
export interface WebhookVerificationResult {
valid: boolean;
reason: string | null;
}
export function verifyApprovaWebhookSignature(input: {
rawBody: string;
signatureHeader: string | undefined;
timestampHeader: string | undefined;
secret: string;
toleranceSeconds?: number;
now?: number;
}): WebhookVerificationResult {
if (!input.signatureHeader) {
return {
valid: false,
reason: 'Missing X-Approval-Signature header.',
};
}
if (!input.timestampHeader) {
return {
valid: false,
reason: 'Missing X-Approval-Timestamp header.',
};
}
if (!/^v1=[0-9a-f]+$/i.test(input.signatureHeader)) {
return {
valid: false,
reason: 'Unsupported signature format.',
};
}
const unixTimestamp = Number(input.timestampHeader);
if (!Number.isFinite(unixTimestamp)) {
return {
valid: false,
reason: 'Webhook timestamp is not a valid Unix epoch seconds value.',
};
}
const toleranceSeconds = input.toleranceSeconds ?? 300;
const now = input.now ?? Date.now();
const ageMs = Math.abs(now - unixTimestamp * 1000);
if (ageMs > toleranceSeconds * 1000) {
return {
valid: false,
reason: 'Webhook timestamp is outside the accepted tolerance window.',
};
}
const providedSignature = input.signatureHeader.replace(/^v1=/i, '');
const expectedSignature = createHmac('sha256', input.secret)
.update(`${input.timestampHeader}.${input.rawBody}`)
.digest('hex');
if (providedSignature.length !== expectedSignature.length) {
return {
valid: false,
reason: 'Webhook signature length mismatch.',
};
}
const isValid = timingSafeEqual(
Buffer.from(providedSignature, 'utf8'),
Buffer.from(expectedSignature, 'utf8'),
);
return {
valid: isValid,
reason: isValid ? null : 'Webhook signature mismatch.',
};
}