-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathzoomSignature.js
More file actions
89 lines (73 loc) · 2.65 KB
/
Copy pathzoomSignature.js
File metadata and controls
89 lines (73 loc) · 2.65 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
79
80
81
82
83
84
85
86
87
88
89
import crypto from 'crypto';
export const DEFAULT_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 300;
export function captureRawBody(req, _res, buffer) {
req.rawBody = buffer;
}
export function buildUrlValidationResponse(plainToken, secretToken) {
if (!secretToken) {
throw new Error('missing_webhook_secret_token');
}
const encryptedToken = crypto
.createHmac('sha256', secretToken)
.update(plainToken)
.digest('hex');
return { plainToken, encryptedToken };
}
export function verifyZoomWebhookSignature(req, secretToken, options = {}) {
return verifyZoomWebhookRequest(req, secretToken, options).ok;
}
export function verifyZoomWebhookRequest(req, secretToken, options = {}) {
const signature = req.headers['x-zm-signature'];
const timestamp = req.headers['x-zm-request-timestamp'];
const rawBody = getRawBodyText(req);
const toleranceSeconds = Number(
options.toleranceSeconds ?? DEFAULT_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS
);
if (!secretToken) return { ok: false, reason: 'missing_webhook_secret_token' };
if (!signature) return { ok: false, reason: 'missing_x_zm_signature' };
if (!timestamp) return { ok: false, reason: 'missing_x_zm_request_timestamp' };
if (!rawBody) return { ok: false, reason: 'missing_raw_body' };
const timestampSeconds = Number(timestamp);
if (!Number.isFinite(timestampSeconds)) {
return { ok: false, reason: 'invalid_x_zm_request_timestamp' };
}
const nowMs = Date.now();
const timestampMs = timestampSeconds * 1000;
const ageSeconds = Math.abs(Math.floor(nowMs / 1000) - timestampSeconds);
if (toleranceSeconds > 0) {
if (ageSeconds > toleranceSeconds) {
return {
ok: false,
reason: 'stale_x_zm_request_timestamp',
ageSeconds,
timestampSeconds,
timestampMs,
receivedAtMs: nowMs
};
}
}
const expected = buildZoomWebhookSignature(rawBody, secretToken, timestamp);
const left = Buffer.from(signature);
const right = Buffer.from(expected);
if (left.length !== right.length || !crypto.timingSafeEqual(left, right)) {
return { ok: false, reason: 'invalid_x_zm_signature' };
}
return {
ok: true,
reason: 'verified',
timestampSeconds,
timestampMs,
receivedAtMs: nowMs,
ageSeconds
};
}
export function buildZoomWebhookSignature(rawBody, secretToken, timestamp) {
const message = `v0:${timestamp}:${rawBody}`;
return `v0=${crypto.createHmac('sha256', secretToken).update(message).digest('hex')}`;
}
function getRawBodyText(req) {
if (req.rawBody) return req.rawBody.toString('utf8');
if (typeof req.body === 'string') return req.body;
if (req.body) return JSON.stringify(req.body);
return '';
}