Skip to content

Commit 0dd5186

Browse files
reeceyangConvex, Inc.
authored andcommitted
docs: add webhook log stream verification example (#43447)
GitOrigin-RevId: 21581cea2bddff9f78c9316d739174f916dc4683
1 parent 61ec13b commit 0dd5186

File tree

1 file changed

+68
-1
lines changed

1 file changed

+68
-1
lines changed

npm-packages/docs/docs/production/integrations/log-streams/log-streams.mdx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,74 @@ logs via POST requests to any URL you configure. The only parameter required to
7171
set up this stream is the desired webhook URL.
7272

7373
A request to this webhook contains as its body a JSON array of events in the
74-
schema defined below.
74+
schema defined below. The request body is signed using HMAC-SHA256 and encoded
75+
as a lowercase hex string, and the resulting signature is included in the
76+
`x-webhook-signature` HTTP header. The HMAC secret is visible in the dashboard
77+
upon configuring the webhook.
78+
79+
To verify the authenticity of a webhook request, sign and encode the request
80+
body using the HMAC secret and
81+
[compare the result in constant time](https://www.chosenplaintext.ca/articles/beginners-guide-constant-time-cryptography.html)
82+
(for instance using
83+
[`SubtleCrypto.verify()`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/verify)
84+
in JavaScript) with the signature included in the request header. Note that the
85+
signature is prefixed with `sha256=`.
86+
87+
For additional security, consider validating that the `timestamp` field of the
88+
log event body falls within an acceptable time range to prevent replay attacks.
89+
90+
```typescript
91+
import { Hono } from "hono";
92+
93+
const app = new Hono();
94+
95+
app.post("/webhook", async (c) => {
96+
const payload = await c.req.json();
97+
const log = payload[0];
98+
99+
// If using JSONL, parse the first line:
100+
// const payload = await c.req.text();
101+
// const log = JSON.parse(payload.split("\n")[0]);
102+
103+
// Validate that the timestamp of the first log is within 5 minutes
104+
if (log.timestamp < Date.now() - 5 * 60 * 1000) {
105+
c.status(403);
106+
return c.text("Request expired");
107+
}
108+
109+
const signature = c.req.header("x-webhook-signature");
110+
if (!signature) {
111+
c.status(401);
112+
return c.text("Unauthorized");
113+
}
114+
115+
const hmacSecret = await crypto.subtle.importKey(
116+
"raw",
117+
new TextEncoder().encode(process.env.WEBHOOK_SECRET!),
118+
{ name: "HMAC", hash: "SHA-256" },
119+
false,
120+
["verify"],
121+
);
122+
const hashPayload = await c.req.arrayBuffer();
123+
124+
// Use constant-time comparison to verify the payload
125+
const isValid = await crypto.subtle.verify(
126+
"HMAC",
127+
hmacSecret,
128+
Uint8Array.fromHex(signature.replace("sha256=", "")),
129+
hashPayload,
130+
);
131+
132+
if (isValid) {
133+
return c.text("Success");
134+
}
135+
136+
c.status(401);
137+
return c.text("Unauthorized");
138+
});
139+
140+
export default app;
141+
```
75142

76143
## Log event schema
77144

0 commit comments

Comments
 (0)