Validating webhook deliveries

You can use a webhook secret to verify that a webhook delivery is from Shopwaive.

Once your server is configured to receive payloads, it will listen for any delivery that's sent to the endpoint you configured. For security reasons, you should only process deliveries from Shopwaive.

To ensure your server only processes deliveries from Shopwaive, you need to:

  1. Create a secret token for a webhook.

  2. Store the token securely on your server.

  3. Validate incoming webhook payloads against the token, to verify they are coming from Shopwaive.

You can create a new webhook with a secret token, or you can add a secret token to an existing webhook. When creating a secret token, you should choose a random string of text with high entropy.

  • To create a new webhook with a secret token, see "Creating webhooks."

  • To add a secret token to an existing webhook, edit the webhook's settings. Under "Secret", type a string to use as a secret key. For more information, see "Editing webhooks."

After creating a secret token, you should store it in a secure location that your server can access. Never hardcode a token into an application or push a token to any repository.

Shopwaive will use your secret token to create a hash signature that's sent to you with each payload. The hash signature will appear in each delivery as the value of the X-Shopwaive-Signature-256 header. For more information, see "Webhook events and payloads."

In your code that handles webhook deliveries, you should calculate a hash using your secret token. Then, compare the hash that Shopwaive sent with the expected hash that you calculated, and ensure that they match.

There are a few important things to keep in mind when validating webhook payloads:

  • Shopwaive uses an HMAC hex digest to compute the hash.

  • The hash signature always starts with sha256=.

  • The hash signature is generated using your webhook's secret token and the payload contents.

  • If your language and server implementation specifies a character encoding, ensure that you handle the payload as UTF-8. Webhook payloads can contain unicode characters.

  • Never use a plain == operator. Instead consider using a method like secure_compare or crypto.timingSafeEqual, which performs a "constant time" string comparison to help mitigate certain timing attacks against regular equality operators, or regular loops in JIT-optimized languages.

You can use the following secret and payload values to verify that your implementation is correct:

  • secret: "It's a Secret to Everybody"

  • payload: "Hello, World!"

If your implementation is correct, the signatures that you generate should match the following signature values:

  • signature: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

  • X-Shopwaive-Signature-256: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

You can use your programming language of choice to implement HMAC verification in your code. Following are some examples showing how an implementation might look in various programming languages.

For example, you can define the following verifySignature function and call it in any JavaScript environment when you receive a webhook payload:

let encoder = new TextEncoder();

async function verifySignature(secret, header, payload) {
    let parts = header.split("=");
    let sigHex = parts[1];

    let algorithm = { name: "HMAC", hash: { name: 'SHA-256' } };

    let keyBytes = encoder.encode(secret);
    let extractable = false;
    let key = await crypto.subtle.importKey(
        "raw",
        keyBytes,
        algorithm,
        extractable,
        [ "sign", "verify" ],
    );

    let sigBytes = hexToBytes(sigHex);
    let dataBytes = encoder.encode(payload);
    let equal = await crypto.subtle.verify(
        algorithm.name,
        key,
        sigBytes,
        dataBytes,
    );

    return equal;
}

function hexToBytes(hex) {
    let len = hex.length / 2;
    let bytes = new Uint8Array(len);

    let index = 0;
    for (let i = 0; i < hex.length; i += 2) {
        let c = hex.slice(i, i + 2);
        let b = parseInt(c, 16);
        bytes[index] = b;
        index += 1;
    }

    return bytes;
}

Last updated