Signature Verification
All webhooks include an Araucaria-Signature header that you must verify
to ensure the request came from Araucaria and hasn't been tampered with.
Signature Format
text
Araucaria-Signature: t=1705760400,v1=5d4f8a3c9e7b2a1d...
The signature header contains:
t: Unix timestamp when the webhook was sentv1: HMAC-SHA256 signature (hex-encoded)
Verification Algorithm
- Extract the timestamp (
t) and signature (v1) from the header - Concatenate the timestamp and raw request body with a period:
{timestamp}.{body} - Compute HMAC-SHA256 of this string using your webhook secret
- Compare the computed signature to
v1using constant-time comparison - Verify the timestamp is within 5 minutes of current time (replay protection)
Node.js Example
javascript
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret, toleranceSec = 300) {
// Parse signature header
const parts = signature.split(',');
const timestampPart = parts.find(p => p.startsWith('t='));
const signaturePart = parts.find(p => p.startsWith('v1='));
if (!timestampPart || !signaturePart) {
return { valid: false, error: 'Invalid signature format' };
}
const timestamp = parseInt(timestampPart.slice(2), 10);
const expectedSignature = signaturePart.slice(3);
// Check timestamp tolerance (replay protection)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSec) {
return { valid: false, error: 'Timestamp outside tolerance' };
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const computedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
const valid = crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(computedSignature)
);
return { valid };
}
Express.js Middleware
javascript
app.post('/webhooks/araucaria',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['araucaria-signature'];
const payload = req.body.toString();
const result = verifyWebhookSignature(
payload,
signature,
process.env.WEBHOOK_SECRET
);
if (!result.valid) {
console.error('Invalid signature:', result.error);
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Process the event
switch (event.type) {
case 'connection.connected':
handleConnectionConnected(event.data);
break;
case 'accounts.updated':
handleAccountsUpdated(event.data);
break;
}
res.status(200).send('OK');
}
);
Python Example
python
import hmac
import hashlib
import time
def verify_webhook_signature(payload, signature, secret, tolerance=300):
parts = signature.split(',')
timestamp_part = next((p for p in parts if p.startswith('t=')), None)
signature_part = next((p for p in parts if p.startswith('v1=')), None)
if not timestamp_part or not signature_part:
return False, 'Invalid signature format'
timestamp = int(timestamp_part[2:])
expected_sig = signature_part[3:]
# Check timestamp tolerance
now = int(time.time())
if abs(now - timestamp) > tolerance:
return False, 'Timestamp outside tolerance'
# Compute signature
signed_payload = f'{timestamp}.{payload}'
computed_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected_sig, computed_sig), None
⚠️ Important
Use the raw request body for verification. Do not parse and re-serialize the JSON, as this may change whitespace and break the signature.