@@ -71,7 +71,74 @@ logs via POST requests to any URL you configure. The only parameter required to
7171set up this stream is the desired webhook URL.
7272
7373A 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