Example webhook handler
Use this example as a boilerplate for your webhook handler. We added extra comments to make it super clear:
const express = require('express')
const router = express.Router()
const crypto = require('crypto')
const { logger } = global
// Create your signing key at https://app.frontitude.com/settings/integrations in the "Webhooks" section
const WEBHOOKS_SIGNING_KEY = process.env.WEBHOOKS_SIGNING_KEY || 'xxx'
router.post('/handle_frontitude_events', async function(req, res) {
// It is important to disable CSRF protection for this endpoint if the framework you use enables them by default.
const { 'webhook-id': webhookId, 'webhook-timestamp': webhookTimestamp, 'webhook-signature': webhookSignature } = req.headers
const { eventType, data } = req.body
logger.info({
message: 'Frontitude webhooks: new event received',
eventType,
webhookId
})
// Compare event's timestamp (in seconds since epoch) against system's timestamp to make sure it's within tolerance in order to prevent timestamp/replay attacks (see: https://en.wikipedia.org/wiki/Replay_attack)
const VALID_TIMESTAMP_TOLERANCE_IN_MINUTES = 5
const currentTimestampInSeconds = Math.floor(new Date().getTime() / 1000)
const minutesAgo = currentTimestampInSeconds - (1000 * 60 * VALID_TIMESTAMP_TOLERANCE_IN_MINUTES)
const minutesLater = currentTimestampInSeconds + (1000 * 60 * VALID_TIMESTAMP_TOLERANCE_IN_MINUTES)
if (webhookTimestamp < minutesAgo || webhookTimestamp > minutesLater) {
logger.error({
message: 'Frontitude webhooks: invalid timestamp',
webhookTimestamp,
minutesAgo,
minutesLater,
request: {
headers: req.headers,
body: req.body
}
})
return res.status(401).send('Invalid timestamp')
}
// Verify event's signature by comparing the signature sent in the webhook headers with the signature computed from the webhook payload
// The content to sign is composed by concatenating the webhook id, timestamp and payload, separated by the full-stop character (.).
signedContent = `${webhookId}.${webhookTimestamp}.${JSON.stringify(req.body)}`
const expectedSignature = crypto
.createHmac('sha256', Buffer.from(WEBHOOKS_SIGNING_KEY, 'base64'))
.update(signedContent)
.digest('base64')
// The expected signature should match one of the signatures sent in the signature header
// Signature header is composed of a list of space delimited signatures and their corresponding version identifiers. The list is most commonly of length one. Though there could be any number of signatures.
// For example: "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo="
const webhookPotentialSignatures = webhookSignature.split(' ').map(signature => signature.split(',')[1])
if (!webhookPotentialSignatures.includes(expectedSignature)) {
logger.error({
message: 'Frontitude webhooks: invalid signature',
expectedSignature,
webhookSignature,
request: {
headers: req.headers,
body: req.body
}
})
return res.status(401).send('Invalid signature')
}
// Process the event
// The way to indicate that a webhook has been processed is by returning a 2xx (status code 200-299) response to the webhook message within a reasonable time-frame (up to 15s). If processing the webhook takes longer than that, it's better to return a 2xx response and process the webhook asynchronously, to avoid Frontitude retrying the webhook delivery.
logger.info({
message: 'Frontitude webhooks: processed event successfully',
eventType,
data
})
res.sendStatus(200)
})
module.exports = router
Last updated