Webhook Security
Webhook Security
Section titled “Webhook Security”Securing your webhook endpoints is critical. This guide covers best practices for verifying and protecting webhook deliveries.
Signature Verification
Section titled “Signature Verification”Every webhook includes a signature in the X-LMIF-Signature header. Always verify this signature.
How Signatures Work
Section titled “How Signatures Work”X-LMIF-Signature: sha256=abc123...The signature is an HMAC-SHA256 hash of the raw request body using your webhook secret.
Verification Examples
Section titled “Verification Examples”import { LMIFClient } from '@lookmaimfamous/lmif';
const lmif = new LMIFClient({ apiKey: process.env.LMIF_API_KEY });
app.post('/webhooks/lmif', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-lmif-signature']; const payload = req.body; // Raw buffer
const isValid = lmif.webhooks.verify( payload, signature, process.env.LMIF_WEBHOOK_SECRET );
if (!isValid) { return res.status(401).send('Invalid signature'); }
// Process webhook...});import crypto from 'crypto';
function verifyWebhookSignature( payload: Buffer | string, signature: string, secret: string): boolean { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex');
const expected = `sha256=${expectedSignature}`;
// Use timing-safe comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}
// Usageapp.post('/webhooks/lmif', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-lmif-signature'] as string;
if (!verifyWebhookSignature(req.body, signature, process.env.LMIF_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); }
// Process webhook...});import hmacimport hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool: expected = 'sha256=' + hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example@app.route('/webhooks/lmif', methods=['POST'])def webhook(): signature = request.headers.get('X-LMIF-Signature') payload = request.get_data()
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET): return 'Invalid signature', 401
# Process webhook...Timestamp Validation
Section titled “Timestamp Validation”Webhooks include a timestamp to prevent replay attacks:
{ "id": "evt_abc123", "type": "box.created", "createdAt": "2024-01-15T10:00:00Z", ...}Reject webhooks older than 5 minutes:
function validateTimestamp(event: WebhookEvent): boolean { const eventTime = new Date(event.createdAt).getTime(); const now = Date.now(); const fiveMinutes = 5 * 60 * 1000;
return now - eventTime < fiveMinutes;}
app.post('/webhooks/lmif', (req, res) => { // After signature verification... const event = JSON.parse(req.body);
if (!validateTimestamp(event)) { console.warn('Webhook timestamp too old:', event.id); return res.status(400).send('Webhook expired'); }
// Process webhook...});Idempotency
Section titled “Idempotency”Webhooks may be delivered multiple times. Handle duplicates safely:
// Use a cache or database to track processed eventsconst processedEvents = new Set<string>();
async function handleWebhook(event: WebhookEvent) { // Check if already processed if (processedEvents.has(event.id)) { console.log('Duplicate webhook, skipping:', event.id); return; }
// For production, use a database const existing = await db.webhookEvents.findOne({ eventId: event.id }); if (existing) { return; }
// Process the event await processEvent(event);
// Mark as processed await db.webhookEvents.insert({ eventId: event.id, processedAt: new Date() }); processedEvents.add(event.id);}Secret Management
Section titled “Secret Management”Store Secrets Securely
Section titled “Store Secrets Securely”# .env (never commit this file)LMIF_WEBHOOK_SECRET=whsec_abc123...Rotate Secrets
Section titled “Rotate Secrets”Rotate your webhook secret periodically:
// Rotate the secretconst newSecret = await lmif.webhooks.rotateSecret('wh_xyz789');
// You have 24 hours to update your code// Both old and new secrets work during this periodconsole.log('New secret:', newSecret);- Go to Settings → Webhooks
- Click your endpoint
- Click Rotate Secret
- Update your code with the new secret within 24 hours
IP Allowlisting
Section titled “IP Allowlisting”For additional security, allowlist LMIF’s IP addresses:
| Environment | IP Addresses |
|---|---|
| Production | Contact support for current IPs |
| Sandbox | Contact support for current IPs |
const ALLOWED_IPS = ['203.0.113.1', '203.0.113.2']; // Example IPs
app.post('/webhooks/lmif', (req, res) => { const clientIp = req.ip || req.headers['x-forwarded-for'];
if (!ALLOWED_IPS.includes(clientIp)) { console.warn('Webhook from unknown IP:', clientIp); return res.status(403).send('Forbidden'); }
// Process webhook...});HTTPS Requirement
Section titled “HTTPS Requirement”Webhook endpoints must use HTTPS in production:
- ✅
https://yoursite.com/webhooks/lmif - ❌
http://yoursite.com/webhooks/lmif
Error Handling
Section titled “Error Handling”Don’t expose internal errors in responses:
app.post('/webhooks/lmif', async (req, res) => { try { // Verify and process... await processWebhook(req.body); res.status(200).send('OK'); } catch (error) { // Log the full error internally console.error('Webhook processing error:', error);
// Return generic error to client res.status(500).send('Internal error'); }});Security Checklist
Section titled “Security Checklist”- Verify signatures on every webhook
- Use timing-safe comparison for signatures
- Validate timestamps to prevent replay attacks
- Handle duplicate deliveries idempotently
- Store secrets in environment variables
- Rotate secrets regularly
- Use HTTPS in production
- Consider IP allowlisting
- Don’t expose internal errors
- Log all verification failures