Webhooks
Security

To secure webhook communication, Sphere Engine uses HMAC (Hash-based Message Authentication Code) for signing messages. This method ensures the authenticity and integrity of each message, verifying its source and unchanged status during transmission.

Secure webhooks using an HMAC signature

Go to the Webhooks - Security section. Then set up and copy the HMAC secret to your application.

Learn more about HMAC on wikipedia or webhooks.fyi.

Every time you receive a webhook, you can verify the authenticity of the payload by comparing the signature in the X-Sphere-Engine-Signature header to the HMAC signature of the payload.

The webhook payload can be verified by following these steps:

  • extract the signature from the X-Sphere-Engine-Signature header,
  • compute the HMAC signature of the payload using the HMAC-SHA256 algorithm and the HMAC secret you copied earlier,
  • compare the computed signature to the signature in the X-Sphere-Engine-Signature header.

To prevent timing attacks, use a constant-time string comparison function to compare the computed signature to the signature in the X-Sphere-Engine-Signature header.

Verify the signature

Below are examples demonstrating how to verify the signature in various programming languages, implementing the steps described above.

// get the secret from an environment variable
$WEBHOOK_SECRET = getenv('WEBHOOK_SECRET') ? getenv('WEBHOOK_SECRET') : 'test-secret';
// get the Sphere Engine signature from the request headers ('X-Sphere-Engine-Signature' header)
$x_sphere_engine_signature = 'ced6bb3f63aebf53f47e19407520ed1c5c65d5011bf67e3e8f3f3fd07b154428';
// get the raw request body (string or bytes, not parsed JSON)
$webhook_message = '[{"origin": "secow", "id": "42fc3ddc-8eb1-4faa-aa3d-238a7a2dd06e", and other fields...}]';

// generate the signature
$signature = hash_hmac('sha256', $webhook_message, $WEBHOOK_SECRET);

// compare the generated signature with the Sphere Engine signature
// if the signatures match, the webhook message is authentic
// REMEMBER: always use a constant-time comparison function to compare the signatures, to prevent timing attacks
if (hash_equals($signature, $x_sphere_engine_signature)) {
    echo 'The webhook message is authentic' . PHP_EOL;
} else {
    echo 'The webhook message is not authentic' . PHP_EOL;
}

// if you want to prevent replay attacks, now is the time that you can save the webhook message ID in the database
// and check if the message ID has already been processed

// try changing the webhook secret, the webhook message or the x_sphere_engine_signature to see if the comparison fails
import hmac
import hashlib
import os

# get the secret from an environment variable
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'test-secret')
# get the Sphere Engine signature from the request headers ('X-Sphere-Engine-Signature' header)
# the string you see here was generated using the 'test-secret' and the webhook message below
x_sphere_engine_signature = 'ced6bb3f63aebf53f47e19407520ed1c5c65d5011bf67e3e8f3f3fd07b154428'
# get the raw request body (string or bytes, not parsed JSON)
webhook_message = '[{"origin": "secow", "id": "42fc3ddc-8eb1-4faa-aa3d-238a7a2dd06e", and other fields...}]'


# convert the webhook message to bytes
msg_as_bytes = webhook_message.encode()
secret_as_bytes = WEBHOOK_SECRET.encode()
# generate the signature
signature = hmac.new(secret_as_bytes, msg=msg_as_bytes, digestmod=hashlib.sha256).hexdigest()

# compare the generated signature with the Sphere Engine signature
# if the signatures match, the webhook message is authentic
# REMEMBER: always use a constant-time comparison function to compare the signatures, to prevent timing attacks
if hmac.compare_digest(signature, x_sphere_engine_signature):
    print('The webhook message is authentic')
else:
    print('The webhook message is not authentic')


# if you want to prevent replay attacks, now is the time that you can save the webhook message ID in the database
# and check if the message ID has already been processed

# try changing the webhook secret, the webhook message or the x_sphere_engine_signature to see if the comparison fails
const crypto = require('crypto');

// get the secret from an environment variable
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'test-secret';
// get the Sphere Engine signature from the request headers ('X-Sphere-Engine-Signature' header)
const x_sphere_engine_signature = 'ced6bb3f63aebf53f47e19407520ed1c5c65d5011bf67e3e8f3f3fd07b154428';
// get the raw request body (string or bytes, not parsed JSON)
const webhook_message = '[{"origin": "secow", "id": "42fc3ddc-8eb1-4faa-aa3d-238a7a2dd06e", and other fields...}]';

// create a hmac object using the secret
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
// update the hmac with the webhook message
hmac.update(webhook_message);
// generate the signature
const signature = hmac.digest('hex');

// compare the generated signature with the Sphere Engine signature
// if the signatures match, the webhook message is authentic
// REMEMBER: always use a constant-time comparison function to compare the signatures, to prevent timing attacks
if (crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(x_sphere_engine_signature, 'hex'))) {
    console.log('The webhook message is authentic');
} else {
    console.log('The webhook message is not authentic');
}

// if you want to prevent replay attacks, now is the time that you can save the webhook message ID in the database
// and check if the message ID has already been processed

// try changing the webhook secret, the webhook message or the x_sphere_engine_signature to see if the comparison fails

Defend against replay attacks

To prevent replay attacks, verify the received webhook message ID against a database of processed messages. If the message ID is already present in the database, the incoming message can be safely disregarded.

Another common way to prevent replay attacks is to check if the webhook message is recent, comparing the message timestamp to the current time. We do not recommend this approach due to a wide time window that the webhook can be delivered in.