Sample code for 30+ languages & platforms
Xbase++ 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 Xbase++ Downloads

Xbase++
LOCAL nSuccess
LOCAL oRawBody
LOCAL cTimestamp
LOCAL cPrimarySignature
LOCAL cSecondarySignature
LOCAL cSignatureVersion
LOCAL cSignatureAlgorithm
LOCAL cPrimaryKey
LOCAL cSecondaryKey
LOCAL oDelivered
LOCAL oNow
LOCAL nAge
LOCAL oSignedBytes
LOCAL oMac
LOCAL cExpectedPrimary
LOCAL cExpectedSecondary
LOCAL oRandom
LOCAL cComparisonKey
LOCAL cPExpected
LOCAL cPReceived
LOCAL cSExpected
LOCAL cSReceived
LOCAL nValidPrimary
LOCAL nValidSecondary
LOCAL oWebhookEvent

nSuccess := 0

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

nSuccess := 0

//  Save the exact HTTP body bytes to this file, without JSON reserialization.
oRawBody := CreateObject("Chilkat.BinData")
nSuccess := oRawBody:LoadFile("qa_data/box_webhook_body.bin")
IF (nSuccess != 1)
    ? oRawBody:LastErrorText
    oRawBody:destroy()
    RETURN
ENDIF

//  Read these fields case-insensitively from the incoming request headers.
//  For this offline example, enter the captured header values.
cTimestamp := "BOX_DELIVERY_TIMESTAMP"
cPrimarySignature := "BOX_SIGNATURE_PRIMARY"
cSecondarySignature := "BOX_SIGNATURE_SECONDARY"
cSignatureVersion := "1"
cSignatureAlgorithm := "HmacSHA256"
//  Retrieve configured keys from protected application configuration.
cPrimaryKey := "BOX_PRIMARY_SIGNATURE_KEY"
cSecondaryKey := "BOX_SECONDARY_SIGNATURE_KEY"
IF ((cSignatureVersion != "1") .OR. (cSignatureAlgorithm != "HmacSHA256"))
    ? "Unsupported signature version or algorithm."
    oRawBody:destroy()
    RETURN
ENDIF

oDelivered := CreateObject("Chilkat.CkDateTime")
nSuccess := oDelivered:SetFromTimestamp(cTimestamp)
IF (nSuccess != 1)
    ? oDelivered:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    RETURN
ENDIF

oNow := CreateObject("Chilkat.CkDateTime")
nSuccess := oNow:SetFromCurrentSystemTime()
IF (nSuccess != 1)
    ? oNow:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    RETURN
ENDIF

nAge := Val(oNow:GetAsUnixTime(0)) - Val(oDelivered:GetAsUnixTime(0))
//  Box recommends rejecting deliveries older than ten minutes.
//  Allow at most 60 seconds of future clock skew; keep the server clock synchronized.
IF ((nAge > 600) .OR. (nAge < -60))
    ? "Delivery timestamp is outside the accepted window."
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    RETURN
ENDIF

oSignedBytes := CreateObject("Chilkat.BinData")
nSuccess := oSignedBytes:AppendBd(oRawBody)
IF (nSuccess != 1)
    ? oSignedBytes:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    RETURN
ENDIF

nSuccess := oSignedBytes:AppendString(cTimestamp, "utf-8")
IF (nSuccess != 1)
    ? oSignedBytes:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    RETURN
ENDIF

oMac := CreateObject("Chilkat.Crypt2")
oMac:MacAlgorithm := "hmac"
oMac:HashAlgorithm := "sha256"
oMac:EncodingMode := "base64"
nSuccess := oMac:SetMacKeyString(cPrimaryKey)
IF (nSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    RETURN
ENDIF

cExpectedPrimary := oMac:MacBdENC(oSignedBytes)
IF (oMac:LastMethodSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    RETURN
ENDIF

nSuccess := oMac:SetMacKeyString(cSecondaryKey)
IF (nSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    RETURN
ENDIF

cExpectedSecondary := oMac:MacBdENC(oSignedBytes)
IF (oMac:LastMethodSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    RETURN
ENDIF

//  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.
oRandom := CreateObject("Chilkat.Prng")
cComparisonKey := oRandom:GenRandom(32, "base64")
IF (oRandom:LastMethodSuccess != 1)
    ? oRandom:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    oRandom:destroy()
    RETURN
ENDIF

nSuccess := oMac:SetMacKeyEncoded(cComparisonKey, "base64")
IF (nSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    oRandom:destroy()
    RETURN
ENDIF

cPExpected := oMac:MacStringENC(cExpectedPrimary)
IF (oMac:LastMethodSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    oRandom:destroy()
    RETURN
ENDIF

cPReceived := oMac:MacStringENC(cPrimarySignature)
IF (oMac:LastMethodSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    oRandom:destroy()
    RETURN
ENDIF

cSExpected := oMac:MacStringENC(cExpectedSecondary)
IF (oMac:LastMethodSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    oRandom:destroy()
    RETURN
ENDIF

cSReceived := oMac:MacStringENC(cSecondarySignature)
IF (oMac:LastMethodSuccess != 1)
    ? oMac:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    oRandom:destroy()
    RETURN
ENDIF

nValidPrimary := (cPrimaryKey != "") .AND. (cPrimarySignature != "") .AND. (cPExpected == cPReceived)
nValidSecondary := (cSecondaryKey != "") .AND. (cSecondarySignature != "") .AND. (cSExpected == cSReceived)
IF ((nValidPrimary == 0) .AND. (nValidSecondary == 0))
    ? "Signature verification failed."
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    oRandom:destroy()
    RETURN
ENDIF

//  Parse JSON only after signature verification.
oWebhookEvent := CreateObject("Chilkat.JsonObject")
nSuccess := oWebhookEvent:Load(oRawBody:GetString("utf-8"))
IF (nSuccess != 1)
    ? oWebhookEvent:LastErrorText
    oRawBody:destroy()
    oDelivered:destroy()
    oNow:destroy()
    oSignedBytes:destroy()
    oMac:destroy()
    oRandom:destroy()
    oWebhookEvent:destroy()
    RETURN
ENDIF

? "Verified event ID: " + oWebhookEvent:StringOf("id")
? "Trigger: " + oWebhookEvent: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.

oRawBody:destroy()
oDelivered:destroy()
oNow:destroy()
oSignedBytes:destroy()
oMac:destroy()
oRandom:destroy()
oWebhookEvent:destroy()