Skip to main content

Webhook Verification

Every webhook from ShieldLabs includes an Assing field — an HMAC-SHA256 signature over the Data object.

How the signature is computed

Assing = HMAC-SHA256( json.Marshal(Data), secret_key )
Where:
  • Data is the full webhook Data object (including Phase when present)
  • secret_key is your domain’s secret key (32 hex chars)
  • Shield.Core uses Go’s json.Marshal on the struct — match field order and types in your language

Verification implementations

package shield

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "time"
)

type ScoreDetail struct {
    Value       int    `json:"Value"`
    Description string `json:"Description"`
}

type WebhookData struct {
    RequestID       string        `json:"RequestID"`
    SessionID       string        `json:"SessionID"`
    CookieID        string        `json:"CookieID"`
    DeviceID        string        `json:"DeviceID"`
    VisitorID       string        `json:"VisitorID"`
    UserHID         string        `json:"UserHID"`
    IP              string        `json:"IP"`
    OS              string        `json:"OS"`
    Country         string        `json:"Country"`
    Score           int           `json:"Score"`
    Details         []ScoreDetail `json:"Details"`
    LastRequestTime time.Time     `json:"LastRequestTime"`
    Phase           string        `json:"Phase,omitempty"`
}

func VerifyWebhook(data WebhookData, assing, secret string) bool {
    b, err := json.Marshal(data)
    if err != nil {
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(b)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(assing), []byte(expected))
}

Important notes

  • The field name is Assing (not Assign or signature)
  • Use a timing-safe comparison to prevent timing attacks
  • Compare hex strings with Buffer.from(assing, 'hex') in Node.js — do not compare raw strings byte-by-byte unless both are hex
  • The HMAC is computed over the JSON serialization of Data — not the entire webhook body
  • Include Phase in the signed payload when present ("initial" or "update")
  • You may receive two webhooks per session: Phase: "initial" then Phase: "update" after WebRTC

Express.js middleware example

import express from 'express';
import crypto from 'crypto';

export function shieldWebhookMiddleware(secret) {
  return (req, res, next) => {
    const { Data, Assing } = req.body;
    if (!Data || !Assing) {
      return res.status(400).json({ error: 'Missing Data or Assing' });
    }
    const body = JSON.stringify(Data);
    const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
    try {
      const valid = crypto.timingSafeEqual(
        Buffer.from(Assing, 'hex'),
        Buffer.from(expected, 'hex')
      );
      if (!valid) return res.status(401).json({ error: 'Invalid signature' });
    } catch {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    req.shieldData = Data;
    next();
  };
}

// Usage
app.post('/webhook', express.json(), shieldWebhookMiddleware(process.env.SHIELD_SECRET), (req, res) => {
  const { Score, UserHID, Phase } = req.shieldData;
  // ... handle score (initial vs update)
  res.status(200).end();
});