Webhook Signature Verification Guide

This guide explains how to implement secure webhook signature verification in Node.js using Express. The verification process ensures that webhook payloads are authentic and haven't been tampered with during transmission.

Overview

When a webhook is sent, the payload is signed with a shared secret using HMAC-SHA256. The signature is included in the request headers. The receiving server must verify this signature before processing the webhook.

Implementation Steps

1. Set Up Environment

First, ensure you have the required dependencies:

npm install express body-parser

Create an environment variable for your webhook signature key:

WEBHOOK_SIGNATURE_KEY=your_signature_key_here

2. Configure Express Server

You'll need to use the raw body parser to access the original payload for signature verification:

import express from 'express';
import bodyParser from 'body-parser';

const app = express();
const rawBodyParser = bodyParser.raw({ type: 'application/json' });

3. Implement Signature Verification

Create a middleware function to verify the webhook signature:

import crypto from 'crypto';

function verifyWebhookSignature(req: any, res: any, next: any) {
  const webhookSecret = process.env.WEBHOOK_SECRET;
  const signature = req.headers['x-webhook-signature'];

  if (!signature) {
    return res.status(401).send('Missing webhook signature');
  }

  // Compute signature from raw payload
  const computedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(req.body)
    .digest('hex');

  // Constant-time comparison to prevent timing attacks
  const isSignatureValid = crypto.timingSafeEqual(
    Buffer.from(computedSignature),
    Buffer.from(signature)
  );

  if (!isSignatureValid) {
    return res.status(403).send('Invalid webhook signature');
  }

  // Attach parsed body to request
  req.parsedPayload = JSON.parse(req.body);
  next();
}

4. Create Webhook Endpoint

Set up your webhook endpoint using the verification middleware:

app.post('/webhook', 
  rawBodyParser,
  verifyWebhookSignature,
  (req, res) => {
    try {
      const payload = req.parsedPayload;
      // Process webhook payload
      handleWebhookEvent(payload);
      res.status(200).send('Webhook processed successfully');
    } catch (error) {
      console.error('Webhook processing error:', error);
      res.status(500).send('Internal server error');
    }
  }
);

Complete Example

Here's a complete example showing how to implement webhook signature verification:

import express from 'express';
import crypto from 'crypto';
import bodyParser from 'body-parser';

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware to parse raw body
const rawBodyParser = bodyParser.raw({ type: 'application/json' });

// Signature verification middleware
function verifyWebhookSignature(req: any, res: any, next: any) {
  const webhookSecret = process.env.WEBHOOK_SECRET;
  const signature = req.headers['x-webhook-signature'];

  if (!signature) {
    return res.status(401).send('Missing webhook signature');
  }

  const computedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(req.body)
    .digest('hex');

  const isSignatureValid = crypto.timingSafeEqual(
    Buffer.from(computedSignature),
    Buffer.from(signature)
  );

  if (!isSignatureValid) {
    return res.status(403).send('Invalid webhook signature');
  }

  req.parsedPayload = JSON.parse(req.body);
  next();
}

// Webhook endpoint
app.post('/webhook', 
  rawBodyParser,
  verifyWebhookSignature,
  (req, res) => {
    try {
      const payload = req.parsedPayload;
      handleWebhookEvent(payload);
      res.status(200).send('Webhook processed successfully');
    } catch (error) {
      console.error('Webhook processing error:', error);
      res.status(500).send('Internal server error');
    }
  }
);

function handleWebhookEvent(payload: any) {
  console.log('Processing webhook:', payload);
  // Implement your webhook processing logic here
}

app.listen(PORT, () => {
  console.log(`Webhook server running on port ${PORT}`);
});

Security Best Practices

  1. Secret Management

    • Store the webhook secret in environment variables

    • Never commit secrets to version control

    • Rotate secrets periodically

  2. Signature Verification

    • Always use constant-time comparison (crypto.timingSafeEqual)

    • Verify signatures before processing payloads

    • Return generic error messages to prevent information leakage

  3. Request Validation

    • Validate request headers

    • Implement request timeout

    • Consider implementing replay protection

  4. Error Handling

    • Log verification failures

    • Don't expose internal errors to clients

    • Implement proper monitoring and alerting

  5. Transport Security

    • Use HTTPS for all webhook endpoints

    • Consider implementing rate limiting

    • Keep dependencies up to date

Testing

To test your webhook endpoint, you can use tools like cURL or Postman. Here's an example cURL command:

# Generate a test signature
SECRET="your_webhook_signature_key_here"
PAYLOAD='{"event":"test","data":"example"}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)

# Send test webhook
curl -X POST http://localhost:3000/webhook \
  -H "Content-Type: application/json" \
  -H "x-webhook-signature: $SIGNATURE" \
  -d "$PAYLOAD"

Troubleshooting

Common issues and solutions:

  1. Invalid Signature Errors

    • Verify the webhook secret matches the one provided in OkDash

    • Ensure payload hasn't been modified in transit

    • Check for encoding issues in the payload

  2. Middleware Order

    • Raw body parser must come before signature verification

    • JSON parser should not be used before signature verification

Last updated