Skip to content
  • There are no suggestions because the search field is empty.

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:

  1. Sets up an Express server on port 3000.
  2. Parses incoming JSON requests and saves the raw body in req.rawBody, which is required for signature verification.
  3. Handles POST requests to /zitadel/webhook.
  4. Extracts the zitadel-signature header
  5. Builds the signed payload
  6. Computes the HMAC-SHA256 signature of the payload using the signing key from your .env file
  7. Compares the computed HMAC signature to the one from the header using crypto.timingSafeEqual for security.
  8. 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}`);
});