Skip to content

Commit 430ee83

Browse files
authored
fix(docs): Enhance webhook signature verification and validation
1 parent ec679b0 commit 430ee83

1 file changed

Lines changed: 24 additions & 13 deletions

File tree

examples/node.md

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,29 +53,40 @@ Use the raw callback form `(payload, req, res)` to access headers directly:
5353
5454
```javascript
5555
import rtms from "@zoom/rtms";
56-
import { createHmac } from "crypto";
56+
import { createHmac, timingSafeEqual } from "crypto";
57+
58+
const WEBHOOK_SECRET = process.env.ZM_RTMS_WEBHOOK_SECRET;
59+
if (!WEBHOOK_SECRET) {
60+
throw new Error("ZM_RTMS_WEBHOOK_SECRET is not set");
61+
}
5762

5863
function verifySignature(body, timestamp, signature) {
59-
const message = `v0:${timestamp}:${body}`;
60-
const expected = "v0=" + createHmac("sha256", process.env.ZM_RTMS_WEBHOOK_SECRET)
61-
.update(message)
64+
if (!signature || !timestamp) return false;
65+
const expected = "v0=" + createHmac("sha256", WEBHOOK_SECRET)
66+
.update(`v0:${timestamp}:${body}`)
6267
.digest("hex");
63-
return expected === signature;
68+
if (signature.length !== expected.length) return false;
69+
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
6470
}
6571

6672
rtms.onWebhookEvent((payload, req, res) => {
67-
const signature = req.headers["x-zm-signature"];
68-
const timestamp = req.headers["x-zm-request-timestamp"];
69-
70-
// Zoom endpoint validation challenge — respond before processing events
71-
if (req.headers["x-zm-webhook-validator"]) {
72-
const token = req.headers["x-zm-webhook-validator"];
73+
// Endpoint validation: Zoom POSTs this once when the webhook URL is set
74+
// or changed. Respond with plainToken + HMAC-SHA256(plainToken, secret)
75+
// to prove the endpoint holds the secret.
76+
if (payload.event === "endpoint.url_validation") {
77+
const plainToken = payload.payload?.plainToken;
78+
const encryptedToken = createHmac("sha256", WEBHOOK_SECRET)
79+
.update(plainToken)
80+
.digest("hex");
7381
res.writeHead(200, { "Content-Type": "application/json" });
74-
res.end(JSON.stringify({ plainToken: token }));
82+
res.end(JSON.stringify({ plainToken, encryptedToken }));
7583
return;
7684
}
7785

78-
if (!signature || !timestamp || !verifySignature(JSON.stringify(payload), timestamp, signature)) {
86+
const signature = req.headers["x-zm-signature"];
87+
const timestamp = req.headers["x-zm-request-timestamp"];
88+
89+
if (!verifySignature(JSON.stringify(payload), timestamp, signature)) {
7990
res.writeHead(401);
8091
res.end(JSON.stringify({ error: "Unauthorized" }));
8192
return;

0 commit comments

Comments
 (0)