JC97
JC97

Reputation: 1620

Zoho subscription validate webhook signature NodeJS

I'm trying to secure my Zoho webhook implementation. I followed this doc: https://www.zoho.com/subscriptions/kb/webhooks/securing-webhooks.html

I don't find it super clear on what to do, but I'm pretty sure that in the end I still did everything what they said.

I DONT have any query parameters. The format is just a default JSON payload NO X-WWW-FORM-URLENCODED.

I tried with following code, but I don't get the correct hash. It's also unclear if I should sort the default payload or not. According to this answer, it's only necessary for form-url-encoded and query parameters, but for plain JSON payload is no processing required. Either way I tried both ways with following implementation as a result:

function computeZohoSignature(query, payload) {
    return crypto
         .createHmac('sha256', process.env.ZOHO_WEBHOOK_SECRET)
         .update(JSON.stringify(payload), 'utf8')
         .digest('hex');
}

function validSignature(signatureHash, computedHash) {
  return signatureHash.length === computedHash.length
     && crypto.timingSafeEqual(Buffer.from(signatureHash), Buffer.from(computedHash));
} 

I also tried wrapping the payload with following function:

function sortObjectByKeys(object) {
  if (!isObject(object)) return object;

  const sortedObj = {};
  Object
    .keys(object)
    .sort()
    .forEach((k) => {
      sortedObj[k] = sortObjectByKeys(object[k]);
    });

  return sortedObj;
}

The sorting works correct, and I even tried with just sorting the "root-keys". Doesn't matter what I try, the hash is never the same. And YES I'm 100% certain the secret is correct, I triple checked that.

Does anyone see what's wrong here or has a working NodeJS implementation of doing this?

Thanks in advance!

Upvotes: 3

Views: 403

Answers (1)

John Slegers
John Slegers

Reputation: 47101

A typical Express setup uses the following configuration for parsing :

app.use(bodyParser.json());

This parser will add the parsed (object) content of a request's body to the body property of the first req parameter of your route handler (req, res) => { ... }.

The hash of your webhook, however, is calculated based on the raw (string) payload. While you could use JSON.stringify to convert your parsed body back to a string, this may result in inconsistencies with the original raw payload.

For example, if your currency is Euro, Zoho will pass the encoded "\u20a" as the value for currency_symbol. However, if you use JSON.stringify to convert your parsed body back to a string, you'll find that it produces the unencoded "€" instead. And because this results in both strings not being identical, they will not produce the same hash.

Without direct access to the raw body, it is very difficult to detect precisely what's the difference between the original raw body & the result of your JSON.stringify and thus how the latter should be transformed for it to be in the same format as the original raw body. The simplest way to overcome this, is to use the verify method of your parser, to add the raw body to eg. the rawBody property of the first req parameter of your route handler, as described in this article :

app.use(bodyParser.json({
  verify: (req, res, buf) => {
    req.rawBody = buf
  }
}))

If you assign the value of req.rawBody to payload, the hash produced by your computeZohoSignature method should now correspond with the signature passed by Zoho!

Upvotes: 1

Related Questions