Sample code for 30+ languages & platforms
React Native Requires Chilkat v11.5.0+

Box: Verify a Captured V2 Webhook Signature

See more Box Examples

Verify raw payload bytes and captured headers using the configured primary and secondary signing keys. The example rejects stale/future timestamps and compares randomized HMACs of the signatures to avoid exposing a signature-prefix timing comparison. It does not host an HTTP listener. The receiver must persist/deduplicate the verified event and acknowledge promptly.

See the Box API guide for setup and the complete workflow.

Chilkat React Native Downloads

React Native
import { BinData, CkDateTime, Crypt2, JsonObject, Prng } from '@chilkat/react-native'

function chilkatExample() {
  // Requires Chilkat 11.5.0 or later and a previously unlocked Chilkat bundle.
  // See Global Unlock Sample.
  // Guide: Box API Guide.

  // Save the exact HTTP body bytes to this file, without JSON reserialization.
  const rawBody = new BinData();
  try {
    rawBody.loadFile('qa_data/box_webhook_body.bin');
  } catch {
    console.log(rawBody.lastErrorText);
    return;
  }

  // Read these fields case-insensitively from the incoming request headers.
  // For this offline example, enter the captured header values.
  const timestamp = 'BOX_DELIVERY_TIMESTAMP';
  const primarySignature = 'BOX_SIGNATURE_PRIMARY';
  const secondarySignature = 'BOX_SIGNATURE_SECONDARY';
  const signatureVersion = '1';
  const signatureAlgorithm = 'HmacSHA256';
  // Retrieve configured keys from protected application configuration.
  const primaryKey = 'BOX_PRIMARY_SIGNATURE_KEY';
  const secondaryKey = 'BOX_SECONDARY_SIGNATURE_KEY';
  if ((signatureVersion !== '1') || (signatureAlgorithm !== 'HmacSHA256')) {
    console.log('Unsupported signature version or algorithm.');
    return;
  }

  const delivered = new CkDateTime();
  try {
    delivered.setFromTimestamp(timestamp);
  } catch {
    console.log(delivered.lastErrorText);
    return;
  }

  const now = new CkDateTime();
  try {
    now.setFromCurrentSystemTime();
  } catch {
    console.log(now.lastErrorText);
    return;
  }

  const age = now.getAsUnixTime(false) - delivered.getAsUnixTime(false);
  // Box recommends rejecting deliveries older than ten minutes.
  // Allow at most 60 seconds of future clock skew; keep the server clock synchronized.
  if ((age > 600) || (age < -60)) {
    console.log('Delivery timestamp is outside the accepted window.');
    return;
  }

  const signedBytes = new BinData();
  try {
    signedBytes.appendBd(rawBody);
  } catch {
    console.log(signedBytes.lastErrorText);
    return;
  }

  try {
    signedBytes.appendString(timestamp, 'utf-8');
  } catch {
    console.log(signedBytes.lastErrorText);
    return;
  }

  const mac = new Crypt2();
  mac.macAlgorithm = 'hmac';
  mac.hashAlgorithm = 'sha256';
  mac.encodingMode = 'base64';
  try {
    mac.setMacKeyString(primaryKey);
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  let expectedPrimary: string;
  try {
    expectedPrimary = mac.macBdENC(signedBytes);
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  try {
    mac.setMacKeyString(secondaryKey);
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  let expectedSecondary: string;
  try {
    expectedSecondary = mac.macBdENC(signedBytes);
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  // Blind both sides with a fresh secret random key before equality comparison.
  // Never compare the original HMAC strings using a prefix-short-circuit comparison.
  // A host-language receiver can instead use its standard constant-time byte comparison.
  const random = new Prng();
  let comparisonKey: string;
  try {
    comparisonKey = random.genRandom(32, 'base64');
  } catch {
    console.log(random.lastErrorText);
    return;
  }

  try {
    mac.setMacKeyEncoded(comparisonKey, 'base64');
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  let pExpected: string;
  try {
    pExpected = mac.macStringENC(expectedPrimary);
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  let pReceived: string;
  try {
    pReceived = mac.macStringENC(primarySignature);
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  let sExpected: string;
  try {
    sExpected = mac.macStringENC(expectedSecondary);
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  let sReceived: string;
  try {
    sReceived = mac.macStringENC(secondarySignature);
  } catch {
    console.log(mac.lastErrorText);
    return;
  }

  const validPrimary = (primaryKey !== '') && ((primarySignature !== '') && (pExpected === pReceived));
  const validSecondary = (secondaryKey !== '') && ((secondarySignature !== '') && (sExpected === sReceived));
  if ((!validPrimary) && (!validSecondary)) {
    console.log('Signature verification failed.');
    return;
  }

  // Parse JSON only after signature verification.
  const webhookEvent = new JsonObject();
  try {
    webhookEvent.load(rawBody.getString('utf-8'));
  } catch {
    console.log(webhookEvent.lastErrorText);
    return;
  }

  console.log(`Verified event ID: ${webhookEvent.stringOf('id')}`);
  console.log(`Trigger: ${webhookEvent.stringOf('trigger')}`);
  // Deduplicate using event.id, not BOX-DELIVERY-ID, which changes on retries.
  // Verify the target belongs to your configured webhook before dispatching work.
}