Documentatie

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.
HeaderWaardeformaat
X-Savebase-Signaturesha256=abcdef0123...
X-Savebase-Eventpost.saved
User-AgentSavebase-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 '', 200

PHP

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 OK binnen 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 → Webhooks als 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), of hash_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.