Webhook Security
Secure your webhook endpoint to prevent unauthorized access and ensure data integrity.
Signature Verification
Every webhook request includes a signature header that you should verify:
| Header | Description |
|---|
X-FYATU-Signature | HMAC-SHA256 signature of the payload |
X-FYATU-Timestamp | Unix timestamp when webhook was sent |
Verification Process
- Get your encryption key from the dashboard
- Compute HMAC-SHA256 of the raw request body
- Compare with the provided signature
Code Examples
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, encryptionKey) {
const expectedSignature = crypto
.createHmac('sha256', encryptionKey)
.update(JSON.stringify(payload))
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
// Express middleware
app.post('/webhooks/fyatu', express.json(), (req, res) => {
const signature = req.headers['x-fyatu-signature'];
const encryptionKey = process.env.FYATU_ENCRYPTION_KEY;
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
if (!verifyWebhookSignature(req.body, signature, encryptionKey)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature valid - process the webhook
console.log('Verified webhook:', req.body.event);
res.status(200).send('OK');
processWebhookAsync(req.body);
});
Timestamp Validation
Prevent replay attacks by checking the timestamp:
const MAX_AGE_SECONDS = 300; // 5 minutes
function isTimestampValid(timestamp) {
const now = Math.floor(Date.now() / 1000);
const webhookTime = parseInt(timestamp, 10);
return Math.abs(now - webhookTime) < MAX_AGE_SECONDS;
}
app.post('/webhooks/fyatu', (req, res) => {
const timestamp = req.headers['x-fyatu-timestamp'];
if (!isTimestampValid(timestamp)) {
return res.status(401).json({ error: 'Webhook too old' });
}
// Continue with signature verification...
});
IP Whitelisting
For additional security, whitelist FYATU’s IP addresses:
const FYATU_IPS = [
'203.0.113.10',
'203.0.113.11',
// Get current IPs from dashboard
];
function isAllowedIP(clientIP) {
return FYATU_IPS.includes(clientIP);
}
app.post('/webhooks/fyatu', (req, res) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (!isAllowedIP(clientIP)) {
console.warn('Webhook from unknown IP:', clientIP);
return res.status(403).json({ error: 'IP not allowed' });
}
// Continue processing...
});
Contact support to get the current list of FYATU webhook IPs.
HTTPS Requirement
Always use HTTPS for your webhook endpoint:
- Encrypts data in transit
- Prevents man-in-the-middle attacks
- Required for production
server {
listen 443 ssl;
server_name api.yourcompany.com;
ssl_certificate /etc/ssl/certs/your-cert.pem;
ssl_certificate_key /etc/ssl/private/your-key.pem;
location /webhooks/fyatu {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Error Handling
Handle verification failures gracefully:
app.post('/webhooks/fyatu', (req, res) => {
try {
// Verify signature
if (!verifySignature(req)) {
// Log for investigation
console.error('Signature verification failed', {
ip: req.ip,
headers: req.headers,
body: req.body
});
// Don't reveal details to potential attacker
return res.status(401).send('Unauthorized');
}
// Process webhook
processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing error:', error);
// Return 500 so FYATU retries
res.status(500).send('Internal error');
}
});
Security Checklist
Verify webhook signatures using HMAC-SHA256
Use timing-safe comparison for signatures
Validate timestamp to prevent replay attacks
Use HTTPS for all webhook endpoints
Consider IP whitelisting for additional security
Store encryption key securely (environment variable)
Log failed verification attempts for monitoring
Don’t expose detailed error messages to callers
Rotating Encryption Keys
If you need to rotate your encryption key:
- Generate a new key in the dashboard
- Update your webhook handler to accept both old and new signatures
- Once all webhooks use the new key, remove the old key
function verifyWithRotation(payload, signature, keys) {
// Try current key first, then old key
for (const key of keys) {
if (verifyWebhookSignature(payload, signature, key)) {
return true;
}
}
return false;
}
const ENCRYPTION_KEYS = [
process.env.FYATU_ENCRYPTION_KEY, // Current
process.env.FYATU_ENCRYPTION_KEY_OLD, // Previous (during rotation)
].filter(Boolean);