Webhook Security
Securing your webhook endpoints is crucial to prevent unauthorized access and ensure the integrity of the events received from BirrLink.
Signature Verification
Every webhook request from BirrLink includes a signature in the BirrLink-Signature header that you should use to verify the request's authenticity:
BirrLink-Signature: t=1678886400,v1=9b21f2b3c286d8a515a5d3e759739a1556d8d2f3a4b5c6d7e8f9a0b1c2d3e4f5
Signature Format
The signature header contains:
t: The timestamp of when the webhook was sent (Unix timestamp)v1: The HMAC-SHA256 signature of the payload using your signing secret
Verification Process
To verify a webhook signature:
- Extract the timestamp and signature from the header
- Prepare the payload string:
t=<timestamp>,v1=<signature> - Compute the HMAC-SHA256 hash of the raw body using your signing secret
- Compare the computed hash with the signature in the header
Code Examples
Node.js:
const crypto = require('crypto');
const webhookSecret = process.env.BIRR_LINK_WEBHOOK_SECRET;
app.post('/webhooks/birrlink', express.raw({type: 'application/json'}), (req, res) => {
const sigHeader = req.headers['birrlink-signature'];
const timestamp = req.headers['birrlink-timestamp'];
const signature = sigHeader.split(',').find(part => part.startsWith('v1=')).split('=')[1];
// Create the payload string to verify
const payloadString = `t=${timestamp},v1=${crypto
.createHmac('sha256', webhookSecret)
.update(req.body)
.digest('hex')}`;
// Verify the signature
const expectedSignature = `t=${timestamp},v1=${signature}`;
const computedSignature = `t=${timestamp},v1=${crypto
.createHmac('sha256', webhookSecret)
.update(req.body)
.digest('hex')}`;
const isValid = crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(expectedSignature)
);
if (!isValid) {
return res.status(400).send('Invalid signature');
}
// Process the webhook event
const event = JSON.parse(req.body);
// ... handle event
res.status(200).send('OK');
});
Python:
import hmac
import hashlib
from flask import Flask, request
webhook_secret = os.getenv('BIRR_LINK_WEBHOOK_SECRET')
@app.route('/webhooks/birrlink', methods=['POST'])
def webhook_handler():
signature_header = request.headers.get('BirrLink-Signature')
timestamp = request.headers.get('BirrLink-Timestamp')
# Extract the signature
signature_parts = signature_header.split(',')
signature = None
for part in signature_parts:
if part.startswith('v1='):
signature = part.split('=')[1]
break
# Compute expected signature
payload = request.get_data()
expected_signature = f"t={timestamp},v1={hmac.new(
webhook_secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()}"
# Verify signature
computed_signature = f"t={timestamp},v1={hmac.new(
webhook_secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()}"
if not hmac.compare_digest(computed_signature, expected_signature):
return 'Invalid signature', 400
# Process the webhook event
event_json = request.get_json()
# ... handle event
return 'OK', 200
Best Practices
Secure Your Signing Secret
- Store your signing secret securely (e.g., environment variables, secrets management)
- Never commit signing secrets to version control
- Rotate your signing secret regularly
- Use different secrets for sandbox and production environments
Validate Event Types
Even with signature verification, validate that the event type is one you expect:
const eventTypes = [
'payment.completed',
'payment.failed',
'refund.created'
];
if (!eventTypes.includes(event.type)) {
console.log('Unexpected event type:', event.type);
return res.status(400).send('Invalid event type');
}
Implement Idempotency
Handle potential duplicate webhook deliveries by implementing idempotency:
// Use the event ID to prevent duplicate processing
const eventId = event.id;
// Check if this event has already been processed
const existingEvent = await db.findWebhookEvent(eventId);
if (existingEvent) {
return res.status(200).send('Event already processed');
}
// Process the event and save the ID
await processEvent(event);
await db.saveWebhookEvent({ id: eventId, processedAt: new Date() });
res.status(200).send('OK');
Time-Based Validation
Check that the webhook timestamp is within an acceptable range (e.g., 5 minutes) to prevent replay attacks:
const now = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 minutes in seconds
if (Math.abs(now - parseInt(timestamp)) > tolerance) {
return res.status(400).send('Webhook timestamp too old');
}
Testing Security
In Development
In development, you can temporarily skip signature verification to test your webhook handling logic, but make sure to implement it before going to production.
Signature Verification Testing
Use test signatures provided in our testing environment to ensure your verification logic works correctly.
Handling Webhook Attempts
Logging
Log all webhook attempts (successful and failed) for debugging and security monitoring:
app.post('/webhooks/birrlink', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['birrlink-signature'];
const timestamp = req.headers['birrlink-timestamp'];
const sourceIp = req.ip;
console.log(`Webhook attempt from ${sourceIp} at ${new Date().toISOString()}`);
// Verify signature
// ... verification logic ...
if (isValid) {
console.log('Webhook signature verified successfully');
// ... process event ...
} else {
console.error('Webhook signature verification failed');
// Possibly block the IP after multiple failed attempts
}
});
Security Monitoring
Anomaly Detection
Monitor for:
- Unusually high volume of webhook requests
- Requests from unexpected IP addresses
- Repeated signature verification failures
- Invalid event types
Incident Response
Have a plan for when webhook security issues are detected:
- Immediately rotate your signing secret
- Investigate the source of unauthorized requests
- Review your webhook handler for vulnerabilities
Remember: Proper webhook security is essential to prevent unauthorized actions based on fake webhook events. Always verify signatures and validate the data before acting on webhook events.