Overview

HookStack signs every webhook request to ensure the authenticity and integrity of the payload. This guide explains how to verify these signatures in your webhook handlers.

Understanding the Signature

Each webhook request from HookStack includes several security-related headers:

X-HookStack-Version: v1.0
X-HookStack-RequestId: <unique-request-id>
X-HookStack-Timestamp: <timestamp>
X-HookStack-Signature: <signature>

The signature is a base64-encoded HMAC SHA-256 hash generated using:

  1. The timestamp from the X-HookStack-Timestamp header
  2. The version from the X-HookStack-Version header
  3. The complete JSON-stringified payload

Verifying the Signature

Here’s how to verify the signature in your webhook handler:

verifyWebhookSignature.ts
function verifyWebhookSignature(
  payload: unknown,
  headers: {
    'x-hookstack-timestamp': string,
    'x-hookstack-version': string,
    'x-hookstack-signature': string
  },
  signingSecret: string
): boolean {
  const timestamp = headers['x-hookstack-timestamp']
  const version = headers['x-hookstack-version']
  const expectedSignature = headers['x-hookstack-signature']

  // Construct the string to sign
  const stringToSign = `${timestamp}:${version}:${JSON.stringify(payload)}`

  // Generate the signature
  const signature = crypto
    .createHmac('sha256', signingSecret)
    .update(stringToSign)
    .digest('base64')

  // Compare signatures using a timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

Using timing-safe comparison when verifying signatures, or checking the timestamp against the current time helps prevent timing attacks.

Best Practices

Example implementation with all best practices:

import { timingSafeEqual } from 'crypto'

function handleWebhook(req: Request, res: Response) {
  const signature = req.headers['x-hookstack-signature']
  const timestamp = Number(req.headers['x-hookstack-timestamp'])
  const version = req.headers['x-hookstack-version']
  const payload = req.body

  // 1. Verify all required headers are present
  if (!signature || !timestamp || !version) {
    return res.status(400).json({ error: 'Missing required headers' })
  }

  // 2. Check timestamp is recent (within 5 minutes)
  const fiveMinutesAgo = Date.now() - 5 * 60 * 1000
  if (timestamp < fiveMinutesAgo) {
    return res.status(400).json({ error: 'Request has expired' })
  }

  // 3. Verify signature
  try {
    const isValid = verifyWebhookSignature(
      payload,
      {
        'x-hookstack-timestamp': timestamp.toString(),
        'x-hookstack-version': version,
        'x-hookstack-signature': signature
      },
      process.env.HOOKSTACK_SIGNING_SECRET!
    )

    if (!isValid) {
      return res.status(401).json({ error: 'Invalid signature' })
    }

    // Process the webhook...
    return res.status(200).json({ received: true })
  } catch (error) {
    console.error('Webhook verification failed:', error)
    return res.status(500).json({ error: 'Verification failed' })
  }
}

The signing secret is provided in your HookStack dashboard under each Destination.

It is required for HTTP-type Destinations only.

Testing Verification

HookStack provides test endpoints and signing secrets in the dashboard to help you verify your implementation:

  1. Use the test webhook feature in the dashboard
  2. Check the test logs to see detailed request/response information
  3. Verify your error handling by sending invalid signatures

Use our SDK to automatically handle webhook verification with built-in security best practices.