Check webhook signatures

Every webhook request Moov sends includes an HMAC-SHA512 signature in the X-Signature header. Recompute it with your signing secret before you process the event, and discard the request if it doesn't match.

Before you start: set up a webhook and copy its signing secret from Developers > Webhooks in the Dashboard. Each webhook, and each mode (test or production), has its own secret.

Verify a request

  1. Read the X-Timestamp, X-Nonce, X-Webhook-ID, and X-Signature headers from the request. Most frameworks lowercase header names.
  2. Build the signed payload by joining the first three values with a pipe: {X-Timestamp}|{X-Nonce}|{X-Webhook-ID}.
  3. Compute an HMAC-SHA512 of that payload using your signing secret, and hex-encode the result.
  4. Compare your value to X-Signature, preferably with a constant-time comparison. If they match, the request came from Moov. If not, return a 4xx and don't process the event.
const hmacSHA512 = require("crypto-js/hmac-sha512");

// Get your signing secret from dashboard.moov.io
const webhookSecret = process.env.WEBHOOK_SECRET;

// Check if the hash of headers match the signature
const isSigned = (timestamp, nonce, webhookId, signature) => {
  const concatHeaders = `${timestamp}|${nonce}|${webhookId}`;
  const checkHash = hmacSHA512(concatHeaders, webhookSecret);

  return signature === checkHash.toString();
}

// Serverless function 
exports.handler = async (event, context, callback) => {
  if (!event.body) {
    console.log("Invalid request");
    callback(null, {
      statusCode: 400,
      body: "Invalid request"
    });
  }

  // Headers are lowercased
  if (!isSigned(
    event.headers["x-timestamp"], 
    event.headers["x-nonce"], 
    event.headers["x-webhook-id"], 
    event.headers["x-signature"])
  ) {
    console.log("Signature is invalid");
    callback(null, {
      statusCode: 400,
      body: "Signature is invalid"
    });
  }

  let webhook;
  try {
    webhook = JSON.parse(event.body);
  } catch (err) {
    console.log("Invalid JSON");
    callback(null, {
      statusCode: 400,
      body: "Invalid JSON"
    });
  }

  // Logs the event message payload
  console.log(event.body);
  callback(null, {
    statusCode: 200
  });
};

Next steps