Verifying Webhooks

This is a guide to show how you're expected to confirm that a webhook comes from Maplerad, this is to prevent malicious actors from sending inappropriate data to your webhook endpoint.

It is very important to verify that webhooks originate from Maplerad.

Verifying signatures manually

Each webhook call includes three headers with additional information that are used for verification:

svix-id: the unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent (e.g. due to a previous failure).
svix-timestamp: timestamp in seconds since epoch.
svix-signature: the Base64 encoded list of signatures (space delimited).

Constructing the signed content

The content to sign is composed by concatenating the id, timestamp and payload, separated by the full-stop character (.). In code, it will look something like:

const signedContent = "${svix_id}.${svix_timestamp}.${body}"

Where body is the raw body of the request. The signature is sensitive to any changes, so even a small change in the body will cause the signature to be completely different. This means that you should not change the body in any way before verifying.

Determining the expected signature

Maplerad uses an HMAC with SHA-256 to sign its webhooks.

So to calculate the expected signature, you should HMAC the signedcontent from above using the base64 portion of your signing secret (this is the part after the whsec prefix) as the key. For example, given the secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw you will want to use MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw.

For example, this is how you can calculate the signature in Node.js:

const crypto = require('crypto');

function getWebhookSignature(svixId, svixTimestamp, body){

    const signedContent = `${svixId}.${svixTimestamp}.${body}`
    const secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH"; // your webhook secret

    // Need to base64 decode the secret
    const secretBytes = Buffer.from(secret.split('_')[1], "base64");
    const signature = crypto
                .createHmac('sha256', secretBytes)
                .update(signedContent)
                .digest('base64');

    console.log(signature);
    return signature
}

function webhookController(req, res){

        const body = req.body
        const svixId = req.headers["svix-id"]
        const svixTimestamp = req.headers["svix-timestamp"]
        const svixSignature = req.headers["svix-signature"]

        const signature = getWebhookSignature(svixId, svixTimestamp, body)
        // check if its valid
        // the rest of your business logic
}
func structToByte(s interface{}) ([]byte, error) {
	buf := new(bytes.Buffer)
	err := json.NewEncoder(buf).Encode(s)
	if err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

type WebhookPayload struct {
}

func verify(signature []byte, secret string, payload *WebhookPayload) bool {
	hash := hmac.New(sha256.New, []byte(secret))
	p, err := structToByte(payload)
	if err != nil {
		fmt.Println(err)
		return false
	}
	hash.Write(p)
	sha := hex.EncodeToString(hash.Sum(nil))
	return hmac.Equal(signature, []byte(sha))
}
<?php
function getWebhookSignature($svixId, $svixTimestamp, $body) {
    $signedContent = "$svixId.$svixTimestamp.$body";
    $secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH"; // your webhook secret

    // Need to base64 decode the secret
    $secretBytes = base64_decode(explode("_", $secret)[1]);
    $signature = base64_encode(hash_hmac('sha256', $signedContent, $secretBytes, true));

    echo $signature;
    return $signature;
}

// Call the function with your data here
?>

This generated signature should match one of the ones sent in the svix-signature header.

The svix-signature header is composed of a list of space delimited signatures and their corresponding version identifiers. The signature list is most commonly of length one. Though there could be any number of signatures.

For example:

v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo=
v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=

Make sure to remove the version prefix and delimiter (e.g. v1,) before verifying the signature.

Please note that to compare the signatures it's recommended to use a constant-time string comparison method in order to prevent timing attacks.

Verify timestamp

As mentioned above, Svix also sends the timestamp of the attempt in the svix-timestamp header. You should compare this timestamp against your system timestamp and make sure it's within your tolerance in order to prevent timestamp attacks.

IP Whitelisting

With this method, you only allow certain IP addresses to access your webhook URL while blocking out others. Maplerad will only send webhooks from the following IP addresses

  1. 54.216.8.72
  2. 54.173.54.49
  3. 52.215.16.239
  4. 52.55.123.25
  5. 52.6.93.106
  6. 63.33.109.123
  7. 44.228.126.217
  8. 50.112.21.217
  9. 52.24.126.164
  10. 54.148.139.208

You should whitelist these IP addresses and consider requests from other IP addresses a counterfeit.