Reputation: 1620
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
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