Verify a signature
Every request is signed with the webhook’s signing secret. Check the signature before you trust the body.
The signature
The signing secret (whsec_…) is shown once, when the webhook is made or its secret replaced. DMU-Signature is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256, keyed with the whole secret, over <t>.<the raw request body>.
Checking it
Check it on the raw bytes, before parsing the JSON, and refuse a t more than five minutes from your clock. A captured request can’t then be replayed later, and a replay within the five minutes carries an event id you have already handled.
JavaScript
import { createHmac, timingSafeEqual } from 'node:crypto'
// `body` is the raw request body as a string: express.raw(), or
// `await request.text()` in a fetch handler. Not a re-serialised object.
export function verify(header, body, secret, toleranceSeconds = 300) {
const parts = (header ?? '').split(',').map((p) => p.trim())
const t = Number(parts.find((p) => p.startsWith('t='))?.slice(2))
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
const expected = createHmac('sha256', secret).update(`${t}.${body}`).digest()
return parts.filter((p) => p.startsWith('v1=')).some((p) => {
const given = Buffer.from(p.slice(3), 'hex')
return given.length === expected.length && timingSafeEqual(given, expected)
})
}
// if (!verify(req.headers['dmu-signature'], rawBody, process.env.DMU_WEBHOOK_SECRET)) return res.status(400).end()Replacing the secret
After the secret is replaced, the old one keeps signing beside the new one for 24 hours: the header then has two v1= values, and either may match. Switch your receiver over within the day.