diff --git a/docs/webhooks/index.html b/docs/webhooks/index.html index 593b112..49a6f50 100644 --- a/docs/webhooks/index.html +++ b/docs/webhooks/index.html @@ -35,7 +35,14 @@
+ Since webhook-signature follows the Standard Webhooks + specification, you can verify it with one of the official + Standard Webhooks libraries, passing your full secret key, including the whsec_ + prefix. For example, in Node.js: +
+import { Webhook } from 'standardwebhooks';
+
+const webhook = new Webhook(secretKey);
+// Throws if the signature is invalid or the timestamp is outside the allowed window
+const event = webhook.verify(rawBody, request.headers);
+ + Always verify against the raw request body, exactly as it was received. If you + parse the JSON and serialize it again before verifying, even small formatting + differences will make the signature fail to match. In Express, for example, use + express.raw({ type: 'application/json' }) instead of + express.json() for your webhook route. +
+import crypto from 'node:crypto';
+
+function verifyWebhookSignature(secretKey, rawBody, headers) {
+ const id = headers['webhook-id'];
+ const timestamp = headers['webhook-timestamp'];
+ const signatureHeader = headers['webhook-signature'];
+ if (!id || !timestamp || !signatureHeader) {
+ return false;
+ }
+
+ const age = Math.abs(Date.now() / 1000 - Number(timestamp));
+ if (!Number.isFinite(age) || age > 5 * 60) {
+ return false;
+ }
+
+ const signingKey = Buffer.from(secretKey.slice('whsec_'.length), 'base64');
+ const signedContent = `${id}.${timestamp}.${rawBody}`;
+ const expected = crypto.createHmac('sha256', signingKey).update(signedContent).digest();
+
+ return signatureHeader.split(' ').some((versionedSignature) => {
+ const [version, signature = ''] = versionedSignature.split(',');
+ const actual = Buffer.from(signature, 'base64');
+ return (
+ version === 'v1' &&
+ actual.length === expected.length &&
+ crypto.timingSafeEqual(actual, expected)
+ );
+ });
+}
+ + Secret keys generated by CodeSignal start with whsec_, + followed by a base64-encoded random value, following the Standard Webhooks format. + If you set a custom secret key that starts with whsec_, + the rest of it must be standard, padded base64 (not URL-safe) that encodes at least + 24 bytes. For example, you can generate one with + echo "whsec_$(openssl rand -base64 32)". +
- For webhooks with a secret key, CodeSignal will sign every request with a - X-CodeSignal-Signature HTTP header, generated using the - following algorithm: + If your secret key doesn't start with whsec_ (for + example, if it was created before this format was introduced), your webhook will + still receive webhook-id, + webhook-timestamp, and + X-CodeSignal-Signature, but not + webhook-signature.
-getSignature(secretKey, endpointUrl, { eventType = '', triggeredOn = '' }) {
+
+ To start receiving webhook-signature, switch to a new key.
+ The new key also changes X-CodeSignal-Signature, and
+ CodeSignal starts using it as soon as you save the webhook, so follow these steps
+ to avoid failed deliveries:
+
+ -
+ Edit your webhook on the
+ Manage Webhooks
+ page, click "Regenerate key", and copy the new key. Don't save yet.
+
+ - Update your endpoint to accept signatures from either the old or new key.
+ - Save the webhook.
+ -
+ Once deliveries are verifying with the new key, remove the old key from your
+ endpoint.
+
+
+
+
+ If your webhook has an empty secret key, it keeps receiving events, but you need to
+ generate a key before you can save changes to it.
+
+ Verifying X-CodeSignal-Signature (legacy)
+
+ CodeSignal continues to send the X-CodeSignal-Signature
+ header, so existing integrations keep working. It's generated using the following
+ algorithm:
+
+ import crypto from 'node:crypto';
+
+function getSignature(secretKey, endpointUrl, { eventType = '', triggeredOn = '' }) {
const plainText = `${endpointUrl}${eventType}${triggeredOn}`;
const hash = crypto.createHmac('sha256', secretKey);
hash.update(plainText);
@@ -208,7 +380,8 @@ Authentication
}
Here, triggeredOn is the timestamp present in every event
- payload.
+ payload. This signature doesn't cover the rest of the request body and doesn't
+ change between retries.
If CodeSignal receives any response other than a 200 OK from the endpoint, the endpoint will be marked unhealthy, and CodeSignal will attempt to resend the - event after a delay (up to 25 attempts total). + event after a delay (up to 25 attempts total). Each retry has the same + webhook-id header as the original attempt, and a new + webhook-timestamp (see + Authentication).
CodeSignal is looking for a 200 OK response from the endpoint URL in order to continue processing