Přejít na obsah
Dokumentace

Návod

Ověření podpisu webhooků

Ověřením podpisu HMAC-SHA256 a časového razítka zkontrolujte, že požadavky webhooků přicházejí od Emailitu, s kódem pro Node.js, Python, PHP, Ruby, Go a Laravel.

Aktualizováno 1. 10. 2026

Požadavky na URL vašeho webhooku může posílat kdokoli, kdo ji zná. Každý požadavek od Emailitu je podepsaný tajným klíčem vašeho webhooku, takže ověření podpisu dokazuje, že požadavek přišel od Emailitu a cestou se nezměnil. Kontrola časového razítka navíc zabrání tomu, aby někdo staré požadavky poslal znovu (útok opakováním). Obojí proveďte dřív, než na jakoukoli událost zareagujete.

Jak podpis funguje

Každý webhook má vlastní tajný klíč: whsec_ a za ním 64 hexadecimálních znaků. Uvidíte ho jednou, když webhook vytvoříte nebo tajný klíč vyměníte, a vrací ho také Načtení webhooku.

U každého požadavku Emailit:

  1. Vezme aktuální unixový čas v sekundách, například 1790846045, a pošle ho v hlavičce X-Emailit-Timestamp.
  2. Sestaví řetězec {timestamp}.{raw body}: časové razítko, tečku a přesné bajty těla požadavku.
  3. Z tohoto řetězce spočítá HMAC-SHA256 a jako klíč použije celý tajný klíč včetně whsec_.
  4. Výsledek zakódovaný hexadecimálně malými písmeny pošle v hlavičce X-Emailit-Signature.

Při ověření váš endpoint zopakuje kroky 2 a 3 s přijatým surovým tělem požadavku a svůj výsledek porovná s hlavičkou porovnáním v konstantním čase. Pak zkontroluje, že časové razítko je čerstvé. Doporučujeme toleranci 5 minut (300 sekund). Každé opakování se podepíše znovu s novým časovým razítkem, takže 5minutové okno opakování neodmítne.

Ověřujte požadavky

Každý příklad načte surové tělo požadavku, požadavky s chybějícím, zastaralým nebo chybným podpisem odmítne s 401 a teprve potom zpracuje pole JSON.

server.js
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.EMAILIT_WEBHOOK_SECRET; // full value, including whsec_
const TOLERANCE_SECONDS = 300;

export function verifyEmailitSignature(rawBody, signature, timestamp) {
  if (!signature || !timestamp) return false;

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!(age <= TOLERANCE_SECONDS)) return false; // also rejects non-numeric timestamps

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${timestamp}.`)
    .update(rawBody) // a Buffer with the exact bytes received
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw() keeps the body as a Buffer. Don't run express.json() on this route.
app.post('/webhooks/emailit', express.raw({ type: 'application/json' }), (req, res) => {
  const valid = verifyEmailitSignature(
    req.body,
    req.get('X-Emailit-Signature'),
    req.get('X-Emailit-Timestamp'),
  );
  if (!valid) return res.status(401).send('Invalid signature');

  const events = JSON.parse(req.body.toString('utf8'));
  // Queue the events for processing, then acknowledge.
  res.sendStatus(204);
});

app.listen(3000);

V Laravelu se u rout v routes/api.php nekontrolují tokeny CSRF a routy dostanou předponu /api, takže URL výše je https://acme.com/api/webhooks/emailit. Pokud routu zaregistrujete v routes/web.php, vyřaďte cestu z ověřování CSRF, jinak Laravel požadavky Emailitu odmítne s 419.

Otestujte ověřování

Před nasazením svou funkci vyzkoušejte na tomto známém příkladu. Zavolejte přímo část s HMAC, protože časové razítko je v minulosti a 5minutovou kontrolou neprojde.

Vstup Hodnota
Tajný klíč whsec_4f2a9c1e7b3d5a8f0e6c2b9d4a7f1e3c5b8d0a2f6e9c3b7d1a5f8e2c4b6d9a0f
Časové razítko 1790846045
Očekávaný podpis 7242b580f77ec488f8ac21b57f092e3db61a431694f13074c7b4c85a4dd04be3

Surové tělo požadavku přesně v této podobě, bez koncového znaku nového řádku:

JSON
[{"event_id":"evt_2xGk7Nq1VbD5sR8tLmW3eYhC6aP","type":"email.delivered"}]

Pak u webhooku použijte Send test a ověřte, že nasazený endpoint přijme skutečný podepsaný požadavek. Viz Nastavení webhooku.

Časté chyby

  • Zpracování před ověřením. Frameworky, které JSON zpracovávají automaticky (express.json(), request.json ve Flasku, params v Rails), vám dají objekt, ne bajty, které Emailit podepsal. Na routě webhooku čtěte surové tělo požadavku.
  • Opětovná serializace JSON. JSON.stringify(req.body) nebo json.dumps(data) vytvoří jiné mezery, pořadí klíčů nebo escapování, takže HMAC nebude souhlasit. Vždy hashujte tělo přesně tak, jak přišlo.
  • Úprava tajného klíče. Použijte celou hodnotu včetně whsec_ jako prostý text. Neodstraňujte předponu a nedekódujte klíč z hexadecimálního zápisu ani z Base64.
  • Podepsání špatného řetězce. Vstupem je časové razítko, tečka a pak tělo. Pokud podepíšete jen tělo nebo části spojíte jiným oddělovačem, ověření selže.
  • Porovnání pomocí ==. Použijte funkci, která porovnává v konstantním čase, například crypto.timingSafeEqual, hmac.compare_digest, hash_equals, Rack::Utils.secure_compare nebo hmac.Equal.
  • Špatně nastavené hodiny serveru. Pokud se hodiny vašeho serveru rozcházejí, platné požadavky vypadnou z tolerance. Synchronizujte je přes NTP.
  • Starý nebo nesprávný tajný klíč. Jakmile vyberete Reset, starý tajný klíč okamžitě přestane fungovat, a to i pro opakování. Každý webhook má vlastní tajný klíč, takže dva endpointy nemohou sdílet jeden.
  • Middleware, který přepisuje tělo. Proxy, API brány nebo dekomprese těla, které změní bajty dřív, než je uvidí váš kód, podpis rozbijí. Ověřujte co nejblíž surovému požadavku.

Požadavky, které neprojdou ověřením, dostanou od vašeho endpointu 401 a opakují se jako každé jiné selhání, takže pokud špatně nastavený tajný klíč opravíte do několika hodin, o nic nepřijdete.

Byla tato stránka užitečná?

Děkujeme za zpětnou vazbu.

Děkujeme, čteme každou zprávu.