Naar de inhoud

Handtekening controleren

Controleren dat een aflevering echt van LeadForge komt met X-LeadForge-Signature.

Elke aflevering van een webhook is ondertekend met het geheim van het adres. Door de handtekening te controleren weet je ontvanger zeker dat het bericht van LeadForge komt en onderweg niet is veranderd. Controleer hem bij elke aflevering, voordat je iets met de gegevens doet.

Headers

Elke aflevering is een POST met deze headers:

Headers

NaamTypeBeschrijving
content-typetekstAltijd application/json.
user-agenttekstAltijd LeadForge-Webhooks/1.0.
x-leadforge-eventtekstDe naam van de gebeurtenis, bijvoorbeeld lead.created.
x-leadforge-deliverytekst (uuid)Het id van de aflevering, gelijk aan id in de body.
x-leadforge-signaturetekstDe handtekening: t=<unix-seconden>,v1=<hex>.
HTTP Voorbeeld
POST /jouw/ontvanger HTTP/1.1
content-type: application/json
user-agent: LeadForge-Webhooks/1.0
x-leadforge-event: lead.created
x-leadforge-delivery: <aflever-id>
x-leadforge-signature: t=1730000000,v1=<64 hex-tekens>

Hoe de handtekening werkt

De header x-leadforge-signature heeft twee delen, gescheiden door een komma:

  • t: het moment van de poging, in seconden sinds 1 januari 1970 (Unix-tijd).
  • v1: de handtekening, 64 tekens in kleine hexadecimale letters.

LeadForge berekent v1 zo:

Tekst
v1 = hex( HMAC-SHA256( sleutel = geheim, bericht = t + "." + ruwe body ) )
  • De sleutel is het volledige geheim als tekst, inclusief whsec_ aan het begin.
  • Het bericht is de waarde van t, een punt en dan de body precies zoals je hem ontvangt.

Je ontvanger rekent hetzelfde uit en vergelijkt de uitkomst met v1. Zijn ze gelijk, dan is de aflevering echt.

Controleren in code

import { createHmac, timingSafeEqual } from 'node:crypto';

// geheim: het volledige geheim van het adres, inclusief whsec_
// ruweBody: de body precies zoals ontvangen, als tekst
// header: de waarde van x-leadforge-signature
export function klopt(geheim, ruweBody, header) {
	let t = null;
	const handtekeningen = [];
	for (const deel of (header ?? '').split(',')) {
		const [sleutel, waarde] = deel.trim().split('=');
		if (sleutel === 't') t = waarde;
		if (sleutel === 'v1' && waarde) handtekeningen.push(waarde);
	}
	if (!t || !handtekeningen.length) return false;

	const verwacht = createHmac('sha256', geheim).update(`${t}.${ruweBody}`, 'utf8').digest('hex');
	const b = Buffer.from(verwacht);
	return handtekeningen.some((v) => {
		const a = Buffer.from(v);
		return a.length === b.length && timingSafeEqual(a, b);
	});
}

// Voorbeeld met Express: lees de body als tekst, niet als JSON.
// app.post('/leadforge', express.text({ type: 'application/json' }), (req, res) => {
// 	if (!klopt(geheim, req.body, req.get('x-leadforge-signature'))) return res.sendStatus(401);
// 	const gebeurtenis = JSON.parse(req.body);
// 	res.sendStatus(200);
// });

Vergelijk de handtekening met een vergelijking die altijd even lang duurt, zoals timingSafeEqual in Node.js of hmac.compare_digest in Python, en niet met === of ==.

Let op

Controleer over de ruwe body, precies zoals je hem ontving. Lees je de body eerst als JSON en zet je hem daarna weer om naar tekst, dan kunnen spaties of de volgorde van velden anders zijn, en klopt de handtekening niet meer.

Problemen oplossen

  • De handtekening klopt nooit. Controleer of je het volledige geheim gebruikt, inclusief whsec_, en of je over de ruwe body rekent.
  • Het geheim is kwijt. Het is niet meer te tonen. Verwijder het adres en voeg het opnieuw toe; zie Het geheim.
  • Dezelfde aflevering komt vaker binnen. Een aflevering kan na een fout opnieuw komen, met hetzelfde id en een nieuwe t. Gebruik x-leadforge-delivery om dubbele afleveringen te herkennen. Zie Aflevering en herhalen.

Verder lezen