I have a service which listens to Hubspot webhook requests (POSTs), and authenticates each request using Hubspot's v3 signature. The code is this (typescript):
const requestSignature = params?.headers?.['x-hubspot-signature-v3']
const requestTimestamp = params?.headers?.['x-hubspot-request-timestamp']
// Validate timestamp
const MAX_ALLOWED_TIMESTAMP = 300000 // 5 minutes in milliseconds
const currentTime = Date.now()
if (currentTime - requestTimestamp > MAX_ALLOWED_TIMESTAMP) {
throw new GeneralError('Hubspot signature v3 timestamp is invalid')
}
// Calculate signature
const clientSecret = <...snip: get secret from secrets store...>
const body = JSON.stringify(params.body)
const source = params.method + params.uri + body + requestTimestamp
const signature = crypto.createHmac('sha256', clientSecret).update(source).digest('base64')
// Validate signature
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(requestSignature))) {
throw new NotAuthenticated('Hubspot signature v3 mismatch')
}
This works most of the time, but occasionally the signatures don't match. It's not because of any missing data, or incorrect data on our side -- I have verified that the correct headers exist, etc. And it's not the call to crypto.timingSafeEqual
-- I have verified that the two signatures do not match.
Has anyone else experienced this before? What could cause just some of the requests from Hubspot to fail signature validation?
@CBroe, in his comment above, provided a hint to the answer, but the problem actually came before JSON.stringify()
: Our app uses global hooks which transforms the request payload in some cases. In this case, one of the string values in the payload JSON had a trailing space, and one of our hooks trims all input data. Executing the above signature validator with the original un-processed request payload solved the problem.