Skip to main content

Overview

This recipe shows you how to use the node.js crypto module to verify the signature of an incoming webhook with an RSA public key. It also includes an example of using the verified function as an express middleware to protect your route from unauthorized webhooks.

Step 1: Import public key

Download the Issuing API Webhook public key and use it in your code to verify notifications sent from our servers.
import express from 'express';
import crypto from 'crypto';

const app = express();
const port = 3000; // You can change this to any port you prefer

// Load the RSA public key
const publicKey = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA24Ee/VaDjmmz0WFeP2Ff
Ish3L2aYiyaHdnphrUiBPRo5ipnLBQkEqnuqHaVakAtNUTJdpwqNgkni4orF4U/d
wsc+l8nd7AuJXs9TxnpDVI3YU1IerwTfs5ORmGewpr9ebnkKY8bcs69kIxZy+4ra
vrk/HBsB317N0LuaoxR7yARORoPuShcwWCc1VlxRKIaZgGHDF3ab145hhYalE/it
GOA2WUjtvM0dogRq6qMKpG0jfMGEK9wdxFGknTr5yTJBuuJvH8x5Oro0fM0vTKMy
qEEXzng/OeVovKZOo7DAQEJfF55O3NqDx+7l1Xfa5BQHyXkqREdSAwZz/7wX/I33
MrLjqAZD1pSlglXwjM0tz6HW7Et7PuV2u9vhOFMLKe9toUTZA46pebcSfPmUoXYF
PDVxnIi5A9bY59Xs7FxDLOPUcafpRTdj/WUliu9btXxfKdWzPuRRNB9mnAymsBj9
vNZmWvpB5bKg29mvI8rP8oc2gs09qYUl1UnIBI2p1GxS+ABbI8eQSenbOxi3u1tR
FZ4QKP1KECJ8VS+cQ+dKQl4gptqRpLhxkpfrZY81vO9IzNov4x4eOdiffhdCg/po
W4ClrhdBVZMcdImmhG9H4W1g5iKHvDWwieOCb+Q36X6N5gqQ/MPf6lX07e1v4vLu
EWljDKDXsmC4+7xu4LTPJE8CAwEAAQ==
-----END PUBLIC KEY-----`;

function verifySignature(content, signature, publicKey) {
  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(content);
  const isVerified = verifier.verify(publicKey, signature, 'base64');
  return isVerified;
}

function verifyWebhookSignature(req, res, next) {
  // Get the signature and payload from the request header
  const signature = req.get('x-access-signature');
  const payload = req.rawBody;

  // Verify the signature using the RSA public key
  const verified = verifySignature(payload, signature, publicKey);

  if (!verified) {
    // If the signature is invalid, return a 401 Unauthorized response
    res.status(401).send('Invalid webhook signature');
  } else {
    // If the signature is valid, proceed to the next middleware function
    next();
  }
}

// Middleware to parse the raw body
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

app.post('/webhook', verifyWebhookSignature, (req, res) => {
  // The incoming webhook has been verified, you can now process it
  console.log('Received verified webhook:', req.body);
  res.status(200).send('Webhook received and verified');
});

// Start the server
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

Step 2: Declare middleware for signature verification

Implement a middleware to automatically verify all webhook requests.
import express from 'express';
import crypto from 'crypto';

const app = express();
const port = 3000; // You can change this to any port you prefer

// Load the RSA public key
const publicKey = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA24Ee/VaDjmmz0WFeP2Ff
Ish3L2aYiyaHdnphrUiBPRo5ipnLBQkEqnuqHaVakAtNUTJdpwqNgkni4orF4U/d
wsc+l8nd7AuJXs9TxnpDVI3YU1IerwTfs5ORmGewpr9ebnkKY8bcs69kIxZy+4ra
vrk/HBsB317N0LuaoxR7yARORoPuShcwWCc1VlxRKIaZgGHDF3ab145hhYalE/it
GOA2WUjtvM0dogRq6qMKpG0jfMGEK9wdxFGknTr5yTJBuuJvH8x5Oro0fM0vTKMy
qEEXzng/OeVovKZOo7DAQEJfF55O3NqDx+7l1Xfa5BQHyXkqREdSAwZz/7wX/I33
MrLjqAZD1pSlglXwjM0tz6HW7Et7PuV2u9vhOFMLKe9toUTZA46pebcSfPmUoXYF
PDVxnIi5A9bY59Xs7FxDLOPUcafpRTdj/WUliu9btXxfKdWzPuRRNB9mnAymsBj9
vNZmWvpB5bKg29mvI8rP8oc2gs09qYUl1UnIBI2p1GxS+ABbI8eQSenbOxi3u1tR
FZ4QKP1KECJ8VS+cQ+dKQl4gptqRpLhxkpfrZY81vO9IzNov4x4eOdiffhdCg/po
W4ClrhdBVZMcdImmhG9H4W1g5iKHvDWwieOCb+Q36X6N5gqQ/MPf6lX07e1v4vLu
EWljDKDXsmC4+7xu4LTPJE8CAwEAAQ==
-----END PUBLIC KEY-----`;

function verifySignature(content, signature, publicKey) {
  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(content);
  const isVerified = verifier.verify(publicKey, signature, 'base64');
  return isVerified;
}

function verifyWebhookSignature(req, res, next) {
  // Get the signature and payload from the request header
  const signature = req.get('x-access-signature');
  const payload = req.rawBody;

  // Verify the signature using the RSA public key
  const verified = verifySignature(payload, signature, publicKey);

  if (!verified) {
    // If the signature is invalid, return a 401 Unauthorized response
    res.status(401).send('Invalid webhook signature');
  } else {
    // If the signature is valid, proceed to the next middleware function
    next();
  }
}

// Middleware to parse the raw body
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

app.post('/webhook', verifyWebhookSignature, (req, res) => {
  // The incoming webhook has been verified, you can now process it
  console.log('Received verified webhook:', req.body);
  res.status(200).send('Webhook received and verified');
});

// Start the server
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

Step 3: Apply middleware to route handler

Attach the middleware to your route handler to utilize it effectively. For better reliability of webhook delivery, consider using a messaging queue like RabbitMQ or Kafka to process webhooks, which will help in separating webhook processing from delivery. This decoupling will ensure the highest possible webhook delivery success rate.
import express from 'express';
import crypto from 'crypto';

const app = express();
const port = 3000; // You can change this to any port you prefer

// Load the RSA public key
const publicKey = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA24Ee/VaDjmmz0WFeP2Ff
Ish3L2aYiyaHdnphrUiBPRo5ipnLBQkEqnuqHaVakAtNUTJdpwqNgkni4orF4U/d
wsc+l8nd7AuJXs9TxnpDVI3YU1IerwTfs5ORmGewpr9ebnkKY8bcs69kIxZy+4ra
vrk/HBsB317N0LuaoxR7yARORoPuShcwWCc1VlxRKIaZgGHDF3ab145hhYalE/it
GOA2WUjtvM0dogRq6qMKpG0jfMGEK9wdxFGknTr5yTJBuuJvH8x5Oro0fM0vTKMy
qEEXzng/OeVovKZOo7DAQEJfF55O3NqDx+7l1Xfa5BQHyXkqREdSAwZz/7wX/I33
MrLjqAZD1pSlglXwjM0tz6HW7Et7PuV2u9vhOFMLKe9toUTZA46pebcSfPmUoXYF
PDVxnIi5A9bY59Xs7FxDLOPUcafpRTdj/WUliu9btXxfKdWzPuRRNB9mnAymsBj9
vNZmWvpB5bKg29mvI8rP8oc2gs09qYUl1UnIBI2p1GxS+ABbI8eQSenbOxi3u1tR
FZ4QKP1KECJ8VS+cQ+dKQl4gptqRpLhxkpfrZY81vO9IzNov4x4eOdiffhdCg/po
W4ClrhdBVZMcdImmhG9H4W1g5iKHvDWwieOCb+Q36X6N5gqQ/MPf6lX07e1v4vLu
EWljDKDXsmC4+7xu4LTPJE8CAwEAAQ==
-----END PUBLIC KEY-----`;

function verifySignature(content, signature, publicKey) {
  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(content);
  const isVerified = verifier.verify(publicKey, signature, 'base64');
  return isVerified;
}

function verifyWebhookSignature(req, res, next) {
  // Get the signature and payload from the request header
  const signature = req.get('x-access-signature');
  const payload = req.rawBody;

  // Verify the signature using the RSA public key
  const verified = verifySignature(payload, signature, publicKey);

  if (!verified) {
    // If the signature is invalid, return a 401 Unauthorized response
    res.status(401).send('Invalid webhook signature');
  } else {
    // If the signature is valid, proceed to the next middleware function
    next();
  }
}

// Middleware to parse the raw body
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

app.post('/webhook', verifyWebhookSignature, (req, res) => {
  // The incoming webhook has been verified, you can now process it
  console.log('Received verified webhook:', req.body);
  res.status(200).send('Webhook received and verified');
});

// Start the server
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

Next Steps