How to Verify Webhook Authenticity
Implement HMAC-SHA256 signature verification to ensure webhook requests are authentic and haven't been tampered with.
How to Verify Webhook Authenticity
Problem
You need to verify that incoming webhook requests are actually from PandaDoc and haven't been tampered with, preventing spoofed requests from malicious actors.
Prerequisites
- Active webhook subscription
- Access to your shared key from the Developer Dashboard(Dev Center)
- Basic understanding of HMAC-SHA256 cryptographic signatures
Solution
Step 1: Retrieve Your Shared Key
- Navigate to the Developer Dashboard
- Locate your webhook subscription
- Copy the shared key (treat this like a password)
Protect Your Shared KeyTreat your shared key like a password to prevent others from generating a webhook with a payload that could pass a signature verification test.
Step 2: Configure Your Webhook URL
When creating your webhook subscription, configure your URL to accept the signature parameter:
https://yourdomain.com/webhook-handler/?signature={signature}
Step 3: Implement Signature Verification
Choose your preferred programming language:
import hmac
import hashlib
def verify_webhook_signature(shared_key, request_body, received_signature):
# Generate expected signature
expected_signature = hmac.new(
str(shared_key).encode('utf-8'),
str(request_body).encode('utf-8'),
digestmod=hashlib.sha256
).hexdigest()
# Use timing-safe comparison
return hmac.compare_digest(expected_signature, received_signature)
<?php
function verifyWebhookSignature($sharedKey, $requestBody, $receivedSignature) {
// Generate expected signature
$expectedSignature = hash_hmac('sha256', $requestBody, $sharedKey);
// Use timing-safe comparison
return hash_equals($expectedSignature, $receivedSignature);
}
?>
using System;
using System.Security.Cryptography;
using System.Text;
public static bool VerifyHMACSHA256(string sharedKey, string requestBody, string receivedSignature) {
byte[] key = Encoding.UTF8.GetBytes(sharedKey);
using (HMACSHA256 hmac256 = new HMACSHA256(key))
{
var hashedSource = hmac256.ComputeHash(Encoding.UTF8.GetBytes(requestBody));
if (hashedSource == null || hashedSource.Length == 0)
{
return false;
}
var expectedSignature = new StringBuilder();
for (int i = 0; i < hashedSource.Length; i++)
{
expectedSignature.AppendFormat("{0:x2}", hashedSource[i]);
}
// Use timing-safe comparison
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expectedSignature.ToString()),
Encoding.UTF8.GetBytes(receivedSignature)
);
}
}
const crypto = require("crypto");
function verifyWebhookSignature(sharedKey, requestBody, receivedSignature) {
// Generate expected signature
const hmac = crypto.createHmac("sha256", sharedKey);
hmac.update(requestBody);
const expectedSignature = hmac.digest("hex");
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(receivedSignature, "hex")
);
}
Step 4: Handle Verification Failures
When signature verification fails:
- Log the event for security monitoring
- Return HTTP 403 (Forbidden) status
- Do not process the webhook payload
- Alert your security team if multiple failures occur
Step 5: Test Your Implementation
- Create a test webhook subscription
- Trigger a document event
- Verify your endpoint correctly validates the signature
- Test with an invalid signature to confirm rejection
Verification
To confirm your implementation works:
- Valid signature test: Process a real webhook and verify it passes
- Invalid signature test: Modify the received signature and confirm it's rejected
- Timing attack protection: Use timing-safe comparison functions
- Error handling: Confirm proper HTTP status codes are returned
Troubleshooting
Common Issues
Signature verification always fails:
- Ensure you're using the raw HTTP body (not parsed JSON)
- Check that your shared key is correct
- Verify the signature is extracted from the correct URL parameter
Security warnings:
- Use
hmac.compare_digest()
in Python orcrypto.timingSafeEqual()
in Node.js - Avoid string comparison operators that are vulnerable to timing attacks
Performance concerns:
- Signature verification is fast (microseconds)
- Cache the shared key rather than fetching it for each request
- Consider implementing rate limiting
Security Best Practices
- Store shared keys securely - Use environment variables or secure vaults
- Use timing-safe comparison - Prevent timing attack vulnerabilities
- Implement logging - Track verification failures for security monitoring
- Regular key rotation - Update shared keys periodically
- IP allowlisting - Combine with signature verification for defense in depth
Related
- How to Set Up Webhook Notifications - Initial webhook configuration
- How to Debug and Monitor Webhooks - Troubleshooting and monitoring
- Webhook Events Reference - Event specifications and IP allowlist
- Understanding Webhooks - Security architecture concepts
Updated 6 days ago