Naar de inhoud

Webhookondertekening voor databronnen ​

Gebruik je een webhook-databron (Lumicast ontvangt HTTP-POST-verzoeken van jouw systeem in plaats van zelf gegevens op te halen), dan kun je eisen dat elk binnenkomend verzoek ondertekend is met een gedeelde geheime sleutel. Niet-ondertekende verzoeken worden geweigerd, zodat niemand die toevallig de webhook-URL kent gegevens naar je schermen kan sturen.

Ondertekening is optioneel. Een webhook-databron zonder ondertekeningssleutel blijft niet-ondertekende POSTs accepteren, precies zoals voorheen. Er verandert niets tot je dit aanzet.

Wanneer zet je dit aan? ​

De webhook-URL is een willekeurige identifier, geen geheim. Iedereen die hem in handen krijgt, via een gedeelde Slack-thread, een logbestand of een screenshot, kan willekeurige JSON naar je databron sturen. Voor dashboards waar weinig van afhangt is dat meestal geen probleem. Zet ondertekening aan als:

  • de databron iets aanstuurt waar mensen op vertrouwen (zaalreserveringen, wachtrijnummers, veiligheidsmeldingen);
  • de afzender een systeem is dat je zelf beheert en kunt aanpassen om verzoeken te ondertekenen;
  • je een URL die mogelijk is uitgelekt wilt afschermen zonder de URL zelf te wijzigen.

Ondertekening inschakelen ​

  1. Ga naar Data koppelingen. Klik op Data koppeling aanmaken en kies Webhook onder Open data, of open een bestaande webhook-databron.
  2. Zet Ondertekende verzoeken vereisen aan.
  3. Klik op Opslaan.

Bij het opslaan maakt Lumicast een ondertekeningssleutel aan en toont die één keer in een pop-up waarmee je hem naar het klembord kopieert. Zet hem in je afzender (bijvoorbeeld in een omgevingsvariabele) voordat je de pop-up sluit: de sleutel in leesbare vorm wordt daarna nooit meer getoond.

WARNING

De sleutel wordt precies één keer getoond. Raak je hem kwijt, dan kun je hem niet meer opvragen. Je moet de sleutel dan rouleren en je afzender bijwerken met de nieuwe.

Zet je de schakelaar uit en sla je op, dan wordt de opgeslagen sleutel gewist en accepteert de webhook weer niet-ondertekende verzoeken.

De sleutel rouleren ​

Open de databron, klik naast de webhook-URL op Ondertekeningssleutel rouleren en bevestig. Er wordt een nieuwe sleutel gegenereerd en één keer getoond. De vorige sleutel werkt direct niet meer, dus werk je afzender meteen bij: elk verzoek dat met de oude sleutel is ondertekend, wordt geweigerd met 401.

Hoe de handtekening werkt ​

Je afzender berekent een HMAC-SHA256 over een korte string die de body aan een tijdstempel koppelt, en stuurt het resultaat samen met die tijdstempel mee:

HeaderWaarde
X-Lumicast-SignatureHex in kleine letters van HMAC-SHA256(secret, "{timestamp}.{body}")
X-Lumicast-TimestampHuidige tijd in milliseconden sinds de Unix-epoch
  • {body} is precies de ruwe body van het verzoek, byte voor byte: dezelfde bytes die je verstuurt. Serialiseer de JSON na het ondertekenen niet opnieuw.
  • {timestamp} is dezelfde waarde die je in de header zet, als string.
  • Verzoeken waarvan de tijdstempel meer dan 5 minuten afwijkt van de klok van Lumicast, worden geweigerd. Dat beperkt het risico op replay-aanvallen en laat ruimte voor een kleine klokafwijking.

Ontbreekt een van de headers, is de tijdstempel verlopen of klopt de handtekening niet, dan antwoordt Lumicast met 401 en negeert het verzoek.

Voorbeeldafzenders ​

Dezelfde uitleg vind je ook in Lumicast zelf: open de databron en klap Hoe verzoeken te ondertekenen uit om een codevoorbeeld te kopiëren waarin je webhook-URL al is ingevuld.

Node.js ​

js
import crypto from 'crypto';

const url = 'https://…/hooks/<identifier>';
const secret = process.env.LUMICAST_WEBHOOK_SECRET;
const body = JSON.stringify({ hello: 'world' });
const timestamp = Date.now().toString();

const signature = crypto.createHmac('sha256', secret)
  .update(`${timestamp}.${body}`)
  .digest('hex');

await fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Lumicast-Signature': signature,
    'X-Lumicast-Timestamp': timestamp,
  },
  body,
});

Python ​

python
import hmac, hashlib, json, os, time, requests

url = "https://…/hooks/<identifier>"
secret = os.environ["LUMICAST_WEBHOOK_SECRET"]
body = json.dumps({"hello": "world"}, separators=(",", ":"))
timestamp = str(int(time.time() * 1000))

signature = hmac.new(
    secret.encode(),
    f"{timestamp}.{body}".encode(),
    hashlib.sha256,
).hexdigest()

requests.post(
    url,
    data=body,
    headers={
        "Content-Type": "application/json",
        "X-Lumicast-Signature": signature,
        "X-Lumicast-Timestamp": timestamp,
    },
)

curl ​

bash
SECRET="${LUMICAST_WEBHOOK_SECRET}"
BODY='{"hello":"world"}'
TS=$(date +%s%3N)   # gebruik zo nodig gdate op macOS
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')

curl -X POST 'https://…/hooks/<identifier>' \
  -H 'Content-Type: application/json' \
  -H "X-Lumicast-Signature: $SIG" \
  -H "X-Lumicast-Timestamp: $TS" \
  --data-raw "$BODY"

Veelgemaakte fouten ​

  • De body na het ondertekenen opnieuw serialiseren. Onderteken precies de bytes die je verstuurt. Een JSON-object netjes opmaken of opnieuw naar een string omzetten verandert de bytes en maakt de handtekening ongeldig.
  • Tijdstempel in seconden in plaats van milliseconden. Date.now() in JS en time.time() * 1000 in Python geven allebei milliseconden. Een tijdstempel in seconden lijkt meteen meer dan 5 minuten oud.
  • Klokafwijking bij de afzender. Loopt de klok van je afzender meer dan 5 minuten uit de pas, dan mislukt elk verzoek met 401. Repareer eerst NTP voordat je de ondertekeningscode gaat debuggen.
  • De sleutel in versiebeheer opslaan. Behandel hem als elk ander wachtwoord: alleen in een omgevingsvariabele of een secrets manager.

Gerelateerd ​

  • Databronnen: overzicht van webhook-databronnen en databronnen die periodiek gegevens ophalen.
  • Webhooks: uitgaande webhooks (Lumicast → jouw server). Ondertekening werkt daar hetzelfde, maar de sleutel hoort bij de webhook, niet bij de databron.