Beveiliging en HMAC-ondertekening
Elk webhookverzoek dat Savebase verstuurt, is ondertekend met HMAC-SHA256, zodat je kunt controleren of het echt van Savebase komt en onderweg niet is gewijzigd. Controleer deze handtekening altijd voordat je een webhookgebeurtenis verwerkt.
Hoe ondertekening werkt
Savebase berekent de handtekening door HMAC-SHA256 uit te voeren op de onbewerkte verzoekinhoud met de volgende waarde van je webhook: Ondertekeningsgeheim als sleutel. Het resultaat wordt verzonden in de X-Savebase-Signature header als sha256=<hex-digest>.
Gebruik de ruwe body
Bereken de HMAC altijd over de ruwe ongewijzigde request-bodybytes — geen opnieuw geserialiseerde JSON. Parseren en herschrijven kan spaties en sleutelvolgorde wijzigen en de handtekening breken.
| Header | Waardeformaat |
|---|---|
X-Savebase-Signature | sha256=abcdef0123... |
X-Savebase-Event | post.saved |
User-Agent | Savebase-Webhook/1.0 |
Node.js / Express
bash
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'utf8'),
Buffer.from(signatureHeader, 'utf8')
);
}
app.post(
'/savebase-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-savebase-signature'];
if (!sig || !verifySignature(req.body, sig, process.env.SAVEBASE_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Event:', event.event, event.data.id);
res.sendStatus(200);
}
);Python / Flask
bash
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['SAVEBASE_WEBHOOK_SECRET']
@app.route('/savebase-webhook', methods=['POST'])
def handle_webhook():
sig_header = request.headers.get('X-Savebase-Signature', '')
raw_body = request.get_data()
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig_header):
abort(401)
payload = request.get_json()
print('Received event:', payload['event'])
return '', 200PHP
bash
<?php
$secret = getenv('SAVEBASE_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_SAVEBASE_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $sigHeader)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
error_log('Received: ' . $event['event']);
http_response_code(200);Aanbevolen beveiligingspraktijken
- Reageer snel. Geef een
200 OKbinnen 10 seconden. Daarna markeert Savebase de levering als mislukt. Verplaats zwaar werk naar een achtergrondtaak. - Log je ondertekeningsgeheim nooit. Behandel het als een wachtwoord. Vernieuw het via
Settings → Webhooksals je denkt dat het is uitgelekt. - Gebruik alleen HTTPS. Savebase weigert gewone HTTP-eindpunten.
- Gebruik een timingveilige vergelijking. Gebruik altijd
crypto.timingSafeEqual(Node),hmac.compare_digest(Python), ofhash_equals(PHP) — nooit een gewone===. - Handel fouten zelf af. Savebase legt het afleverresultaat vast, maar de huidige implementatie probeert mislukte verzoeken niet automatisch opnieuw en speelt ze ook niet opnieuw af.