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 @@

Table of Contents

  • Usage
  • @@ -162,12 +174,13 @@

    How do I create a webhook?

  • Fill out all of the required fields, as well as optional ones if you need them. - We will generate a Secret key automatically, but you can - overwrite it with a custom one. + We will generate a Secret key automatically. You can replace it + with a custom one, but we recommend keeping the generated key (see + Secret key format). Create webhook modal
  • @@ -195,12 +208,171 @@

    How do I create a webhook?

    Usage

    Authentication

    +
    + CodeSignal sends the following HTTP headers with every webhook request, so that you + can verify that it came from CodeSignal and detect duplicate or replayed requests: +
      +
    • + webhook-id: A unique identifier for the event. + It stays the same when a delivery is retried, so you can use it to skip events + you've already processed. If several of your webhooks are notified about the + same event, CodeSignal sends the same webhook-id to + each of their endpoints. Treat it as an opaque string, since its format may + vary. +
    • +
    • + webhook-timestamp: When this delivery attempt + was sent, in seconds since the Unix epoch. Each retry has a new timestamp. +
    • +
    • + webhook-signature: A signature of the event ID, + timestamp, and full request body, as defined by the + Standard Webhooks + specification. This header is only sent if your secret key is in the + Standard Webhooks format. +
    • +
    • + X-CodeSignal-Signature: A + legacy signature that covers the endpoint URL, event + type, and triggeredOn timestamp, but not the rest of + the request body. +
    • +
    + We recommend verifying webhook-signature, since it + covers the full request body and supports replay detection. +
    +

    Verifying webhook-signature

    +

    + 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. +

    +
    + To verify the signature without a library: +
      +
    1. + Reject the request if any of the webhook-id, + webhook-timestamp, or + webhook-signature headers are missing, or if + webhook-timestamp is too far from the current time + (for example, more than five minutes), to protect against replayed requests. +
    2. +
    3. + Remove the whsec_ prefix from your secret key and + base64-decode the rest to get the signing key. +
    4. +
    5. + Join the webhook-id header, the + webhook-timestamp header, and the raw request body + with periods (.) in between. +
    6. +
    7. + Compute the HMAC-SHA256 of that string using the signing key, and + base64-encode the result. +
    8. +
    9. + The webhook-signature header contains one or more + space-separated signatures, each in the form + v1,<signature>. Compare your result with each + v1 signature using a constant-time comparison. +
    10. +
    +
    +
    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 key format

    +

    + 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: +
      +
    1. + Edit your webhook on the + Manage Webhooks + page, click "Regenerate key", and copy the new key. Don't save yet. +
    2. +
    3. Update your endpoint to accept signatures from either the old or new key.
    4. +
    5. Save the webhook.
    6. +
    7. + Once deliveries are verifying with the new key, remove the old key from your + endpoint. +
    8. +
    +
    +

    + 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.

    @@ -226,7 +399,10 @@

    Errors and retry policy

    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