Verify Webhook Signatures

Verify Webhook Signatures

To protect your application against man-in-the-middle and replay attacks it is essential to verify webhook signatures. Verification ensures that the webhook payloads were actually sent by Box and that the contents of the payloads have not been changed in transport.

Verify with SDK

Although it is possible to verify SDKs manually using your own code, convenience methods are provided in our SDKs.

.NET
using Box.V2.Managers;

var body = "{\"type\":\"webhook_event\",\"webhook\":{\"id\":\"1234567890\"},\"trigger\":\"FILE.UPLOADED\",\"source\":{\"id\":\"1234567890\",\"type\":\"file\",\"name\":\"Test.txt\"}}";
var headers = new Dictionary<string, string>()
{
    { "box-delivery-id", "f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f" },
    { "box-delivery-timestamp", "2020-01-01T00:00:00-07:00" },
    { "box-signature-algorithm", "HmacSHA256" } ,
    { "box-signature-primary", "6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=" },
    { "box-signature-secondary", "v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=" },
    { "box-signature-version", "1" }
};
var primaryKey = "Fd28OJrZ8oNxkgmS7TbjXNgrG8v";
var secondaryKey = "KWkROAOiof4zhYUHbAmiVn63cMj"

bool isValid = BoxWebhooksManager.VerifyWebhook(
    deliveryTimestamp: headers["box-delivery-timestamp"],
    signaturePrimary: headers["box-signature-primary"],
    signatureSecondary: headers["box-signature-secondary"],
    payload: body,
    primaryWebhookKey: primaryKey,
    secondaryWebhookKey: secondaryKey
);
Java
// Webhook message contents are shown for demonstration purposes
// Normally these would come from your HTTP handler

// Webhook message HTTP body
String messagePayload = "{"
    + "\"type\":\"webhook_event","
    + "\"webhook\":{"
    +   "\"id\":\"1234567890\""
    + "},"
    + "\"trigger\":\"FILE.UPLOADED\","
    + "\"source\":{"
    +   "\"id\":\"1234567890\","
    +   "\"type\":\"file\","
    +   "\"name\":\"Test.txt\""
    + "}}";

// Webhook message HTTP headers
Map<String, String> messageHeaders = new HashMap<String, String>();
headers.put("BOX-DELIVERY-ID", "f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f");
headers.put("BOX-DELIVERY-TIMESTAMP", "2020-01-01T00:00:00-07:00");
headers.put("BOX-SIGNATURE-ALGORITHM", "HmacSHA256");
headers.put("BOX-SIGNATURE-PRIMARY", "6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=");
headers.put("BOX-SIGNATURE-SECONDARY", "v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=");
headers.put("BOX-SIGNATURE-VERSION", "1");

// Your application's webhook keys, obtained from the Box Developer Console
String primaryKey = "4py2I9eSFb0ezXH5iPeQRcFK1LRLCdip";
String secondaryKey = "Aq5EEEjAu4ssbz8n9UMu7EerI0LKj2TL";

BoxWebHookSignatureVerifier verifier = new BoxWebHookSignatureVerifier(primaryKey, secondaryKey);
boolean isValidMessage = verifier.verify(
    headers.get("BOX-SIGNATURE-VERSION"),
    headers.get("BOX-SIGNATURE-ALGORITHM"),
    headers.get("BOX-SIGNATURE-PRIMARY"),
    headers.get("BOX-SIGNATURE-SECONDARY"),
    messagePayload,
    headers.get("BOX-DELIVERY-TIMESTAMP")
);

if (isValidMessage) {
    // Message is valid, handle it
} else {
    // Message is invalid, reject it
}
Python
body = b'{"webhook":{"id":"1234567890"},"trigger":"FILE.UPLOADED","source":{"id":"1234567890","type":"file","name":"Test.txt"}}'
headers = {
    'box-delivery-id': 'f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f',
    'box-delivery-timestamp': '2020-01-01T00:00:00-07:00',
    'box-signature-algorithm': 'HmacSHA256',
    'box-signature-primary': '4KvFa5/unRL8aaqOlnbInTwkOmieZkn1ZVzsAJuRipE=',
    'box-signature-secondary': 'yxxwBNk7tFyQSy95/VNKAf1o+j8WMPJuo/KcFc7OS0Q=',
    'box-signature-version': '1',
}
is_validated = Webhook.validate_message(body, headers, primary_key, secondary_key)
print('The webhook message is validated to: {0}'.format(is_validated))
Node
const BoxSDK = require('box-node-sdk');
let body = '{"type":"webhook_event","webhook":{"id":"1234567890"},"trigger":"FILE.UPLOADED","source":{"id":"1234567890","type":"file","name":"Test.txt"}}',
	headers = {
		'box-delivery-id': 'f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f',
		'box-delivery-timestamp': '2020-01-01T00:00:00-07:00',
		'box-signature-algorithm': 'HmacSHA256',
		'box-signature-primary': '6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=',
		'box-signature-secondary': 'v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=',
		'box-signature-version': '1'
	},
	primaryKey = 'SamplePrimaryKey',
	secondaryKey = 'SampleSecondaryKey';

let isValid = BoxSDK.validateWebhookMessage(body, headers, primaryKey, secondaryKey);
if (isValid) {
	// message is valid, accept
} else {
	// message is NOT valid, reject
}

Verify manually

If using our SDKs is not an option the following steps describe the basics of how to verify a signature.

1. Ensure valid timestamp

Firstly, ensure that the timestamp in the BOX-DELIVERY-TIMESTAMP header of the payload is no older than ten minutes.

Node
var timestamp = headers['BOX-DELIVERY-TIMESTAMP'];
var date = Date.parse(timestamp);
var expired = Date.now() - date > 10*60*1000;
Python
import dateutil.parser
import pytz
import datetime

timestamp = headers["BOX-DELIVERY-TIMESTAMP"]
date = dateutil.parser.parse(timestamp).astimezone(pytz.utc)

now = datetime.datetime.now(pytz.utc)
delta = datetime.timedelta(minutes=10)
expiry_date = now - deltaMinutes

expired = date >= expiry_date

2. Calculate HMAC signature

Then, calculate the HMAC of the payload using either of the two signatures found in the application's configuration on the developer console.

Make sure to append the bytes of the body of the payload first, and then the bytes of the timestamp found in the BOX-DELIVERY-TIMESTAMP header.

Node
var crypto = require('crypto');

var primaryKey = '...';
var secondaryKey = '...';

var payload = '{"type":"webhook_event"...}';

var hmac1 = crypto.createHmac('sha256', primaryKey);
hmac1.update(payload);
hmac1.update(timestamp);

var hmac2 = crypto.createHmac('sha256', secondaryKey);
hmac2.update(payload);
hmac2.update(timestamp);
Python
import hmac
import hashlib

primary_key = '...'
secondary_key = '...'

payload = "{\"type\":\"webhook_event\"...}"

bytes = bytes(payload, 'utf-8') + bytes(timestamp, 'utf-8')

hmac1 = hmac.new(primary_key, bytes, hashlib.sha256).digest()
hmac2 = hmac.new(secondary_key, bytes, hashlib.sha256).digest()

4. Convert to Base64

Make sure to convert the HMAC to a Base64 encoded digest.

Node
var digest1 = hmac1.digest('base64');
var digest2 = hmac2.digest('base64');
Python
import base64

digest1 = base64.b64encode(hmac1)
digest2 = base64.b64encode(hmac2)

5. Compare signatures

Finally, compare the encoded digest with the value of the BOX-SIGNATURE-PRIMARY or BOX-SIGNATURE-SECONDARY headers.

Make sure to compare the value of the BOX-SIGNATURE-PRIMARY header to the digest created with the primary key, and the value of the BOX-SIGNATURE-SECONDARY header to the digest created with the secondary key.

Node
var signature1 = headers['BOX-SIGNATURE-SECONDARY'];
var signature2 = headers['BOX-SIGNATURE-PRIMARY'];

var primarySignatureValid = digest1 === signature1
var secondarySignatureValid = digest2 === signature2

var valid = !expired && (primarySignatureValid || secondarySignatureValid)
Python
signature1 = headers["BOX-SIGNATURE-SECONDARY"]
signature2 = headers["BOX-SIGNATURE-PRIMARY"]

primary_sig_valid = digest1 === signature1
secondary_sig_valid = digest2 === signature2

valid = !expired && (primary_sig_valid || secondary_sig_valid)

HTTP header names are case insensitive and your client should ideally convert all header names to a standardized lowercase or uppercase format before trying to determine the value of a header.