NewMeet PingPage — status pages for your users
Every webhook endpoint is a publicly accessible URL. If someone discovers it, they can send fake events to your server. Without authentication, your application has no way to distinguish a legitimate webhook from a forged one.
This isn't a theoretical risk. Webhook URL patterns are predictable (/webhooks/stripe, /api/hooks/github), and attackers actively scan for them.
The Three Approaches
1. HMAC Signatures (Recommended)
HMAC (Hash-based Message Authentication Code) is the gold standard for webhook authentication. The sender signs each request using a shared secret, and your server verifies the signature before processing.
How it works:
Provider Your Server
│ │
│ POST /webhooks/stripe │
│ X-Signature: sha256=abc123... │
│ Body: {"event": "payment"} │
│ ─────────────────────────────► │
│ │
│ Compute HMAC of body
│ using shared secret
│ Compare with header
│ │
│ 200 OK │
│ ◄───────────────────────────── │
Implementation:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const expectedBuffer = Buffer.from(expected, 'hex');
const signatureBuffer = Buffer.from(signature, 'hex');
if (expectedBuffer.length !== signatureBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
}
// Express middleware
app.post('/webhooks/stripe', (req, res) => {
const signature = req.headers['x-signature']
?.replace('sha256=', '');
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
const rawBody = req.rawBody; // Need raw body, not parsed
if (!verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature valid — process the event
handleEvent(req.body);
res.status(200).json({ received: true });
});
Critical details:
- Use the raw body — verifying against the parsed/re-serialized body will fail because JSON serialization isn't deterministic
- Use constant-time comparison —
crypto.timingSafeEqualprevents timing attacks - Store secrets securely — environment variables, not source code
Who uses HMAC:
- Stripe (
Stripe-Signatureheader) - GitHub (
X-Hub-Signature-256header) - Shopify (
X-Shopify-Hmac-SHA256header) - HookWatch (
X-HookWatch-Signatureheader)
2. OAuth 2.0 Bearer Tokens
Some webhook providers authenticate using OAuth tokens. Instead of signing the payload, the sender includes a bearer token that your server validates against the provider's authorization server.
app.post('/webhooks/provider', async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Missing token' });
}
// Validate token with the provider
const isValid = await validateToken(token);
if (!isValid) {
return res.status(401).json({ error: 'Invalid token' });
}
handleEvent(req.body);
res.status(200).json({ received: true });
});
Pros:
- Token rotation without changing webhook configuration
- Fine-grained scopes and permissions
- Standard protocol with library support
Cons:
- Requires a network call to validate each webhook (unless using JWTs)
- More complex setup than HMAC
- Latency added by token validation
Who uses OAuth:
- Microsoft Graph
- Zoom
- Salesforce
3. API Keys
The simplest approach: include a secret key as a header or query parameter. Your server checks that the key matches.
app.post('/webhooks/service', (req, res) => {
const apiKey = req.headers['x-api-key'];
if (apiKey !== process.env.WEBHOOK_API_KEY) {
return res.status(401).json({ error: 'Invalid API key' });
}
handleEvent(req.body);
res.status(200).json({ received: true });
});
Pros:
- Dead simple to implement
- No cryptographic operations
- Easy to rotate
Cons:
- Key is sent in every request — if intercepted, an attacker can forge webhooks
- No payload integrity — even with a valid key, the body could be tampered with in transit
- Must use HTTPS (but you should always use HTTPS anyway)
Comparison
| Factor | HMAC | OAuth | API Key |
|---|---|---|---|
| Payload integrity | Yes | No | No |
| Implementation complexity | Medium | High | Low |
| Network calls required | None | Yes (unless JWT) | None |
| Replay protection | With timestamp | With token expiry | No |
| Industry adoption | Very high | Medium | Low |
Preventing Replay Attacks
Even with valid signatures, an attacker who captures a webhook request can replay it. Defend against this by checking the timestamp:
function verifyTimestamp(timestamp, toleranceSeconds = 300) {
const webhookTime = new Date(timestamp).getTime();
const now = Date.now();
const difference = Math.abs(now - webhookTime);
return difference <= toleranceSeconds * 1000;
}
app.post('/webhooks/stripe', (req, res) => {
const timestamp = req.headers['x-webhook-timestamp'];
// Reject webhooks older than 5 minutes
if (!verifyTimestamp(timestamp)) {
return res.status(401).json({ error: 'Webhook too old' });
}
// Include timestamp in HMAC verification
const signedPayload = timestamp + '.' + req.rawBody;
if (!verifyWebhookSignature(signedPayload, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
handleEvent(req.body);
res.status(200).json({ received: true });
});
Testing Authentication with HookWatch
The HookWatch Sandbox includes a built-in Signature Tester that lets you:
- Enter your webhook signing secret
- Paste or craft a payload
- Generate the correct HMAC signature
- Verify your handler accepts it
This makes it easy to test your verification logic without setting up the full provider integration.
# Use the CLI to send a signed test webhook
hookwatch endpoints create \
--name "Signed Endpoint" \
--destination "https://myapp.com/webhooks" \
--signing-secret "whsec_your_secret_here"
Recommendations
- Always use HMAC signatures when the provider supports it
- Verify the raw body — never the parsed version
- Use constant-time comparison to prevent timing attacks
- Check timestamps to prevent replay attacks
- Rotate secrets periodically and when team members leave
- Log verification failures — they could indicate an attack or a misconfiguration
- Always use HTTPS — regardless of which authentication method you use
Tags: security, authentication, hmac, oauth, webhooks