🔀 Sequence diagram
🔐 Security policy
Header signature
We sign the webhook events by including a signature in each event’s Leeway_Signature
header.
Each webhook has a secret autogenerated and shared between us and the end user. Then, to sign the event, we compute an HMAC-SHA256 of the timestamp and the event’s body:
sha256 = HMAC_SHA256(timestamp + “.” + eventBody)
Finally, the Leeway_Signature
header has the following format:
Leeway-Signature: t=1492774577, sha256=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
with t
the timestamp and sha256
the previously computed hash.
Note that newlines have been added for clarity, but a real Leeway_Signature
header is on a single line.
Signature check
When a client receive a webhook event, he should check that the signature is valid. Here is an example of how to do it:
import {createHmac, timingSageEqual} from 'crypto';
function checkSignature(signature, secret, body) {
const timestamp = signature[0].split('=')[1];
const sig = Buffer.from(signature[1].split('=')[1], 'utf8');
const hmac = createHmac('sha256', secret);
const digest = Buffer.from(hmac.update(timestamp + '.' + JSON.stringify(body)).digest('hex'), 'utf8');
if (sig.length !== digest.length || !timingSafeEqual(digest, sig)) {
return false;
} else {
return true;
}
}
Note that it’s recommended to use a time constant-time string comparison to avoid timing attack vulnerability, that’s why we are using here timingSafeEqual
instead of ===
(see https://en.wikipedia.org/wiki/Timing_attack)
Moreover, the client should check if the difference between the received timestamp et the current timestamp is within his tolerance (this is to avoid replay attack).
🔂 Retry policy
If the webhook respond with an other code than a 2xx
or if the webhook take too much time (the timeout is set to 3s for the moment but the value may change), we consider the try as failed.
In this case, if the number of tries for this WebhookEvent
is less than the max allowed (currently MAX_RETRIES = 3
), we publish a new event in the queue with a delay of 5min (same here, value may change).
Otherwise, we don’t retry and disable the webhook.