How to validate ZITADEL Actions V2 signature with Node.js
This article provides a sample code snippet to validate the signature header from an Actions V2 webhook using Node.js
This is a Node.js Express server that listens for incoming ZITADEL Action V2 webhook requests and validates their signature using HMAC-SHA256 to ensure the request is authentic, by doing the following:
- Sets up an Express server on port
3000
. - Parses incoming JSON requests and saves the raw body in
req.rawBody
, which is required for signature verification. - Handles POST requests to
/zitadel/webhook
. - Extracts the
zitadel-signature
header - Builds the signed payload
- Computes the HMAC-SHA256 signature of the payload using the signing key from your
.env
file - Compares the computed HMAC signature to the one from the header using
crypto.timingSafeEqual
for security. - Rejects the request if the signature is invalid (
400 Invalid signature
), or continues if it's valid
const express = require('express');
const crypto = require('crypto');
require('dotenv').config();
const app = express();
const port = 3000;
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody=buf.toString('utf8');
}
}));
app.post('/zitadel/webhook', async (req, res) => {
// Get the webhook signature from the header
const signatureHeader=req.headers['zitadel-signature'];
if (!signatureHeader) {
console.error("Missing signature");
return res.status(400).send('Missing signature');
}
// Get the signing key from the .env file
// This is obtained when the target is created
const SIGNING_SECRET=process.env.SIGNING_KEY;
const elements=signatureHeader.split(',');
const timestamp=elements.find(e=>e.startsWith('t=')).split('=')[1];
const signature=elements.find(e=>e.startsWith('v1=')).split('=')[1];
const signedPayload=`${timestamp}.${req.rawBody}`;
const hmac=crypto.createHmac('sha256', SIGNING_SECRET)
.update(signedPayload)
.digest('hex');
constisValid=crypto.timingSafeEqual(
Buffer.from(hmac),
Buffer.from(signature)
);
if (!isValid) {
console.error("Invalid signature");
return res.status(400).send('Invalid signature');
}
// Signature validated, continue the execution..
res.status(200).send('OK');
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});