Podpis požadavku

Každý webhook požadavek od Emailit obsahuje hlavičku X-Emailit-Signature s HMAC-SHA256 podpisem a hlavičku X-Emailit-Timestamp s Unix časovým razítkem okamžiku podepsání požadavku. Tento podpis byste měli ověřit, abyste zajistili, že je požadavek autentický a nebyl pozměněn.

Jak to funguje

  1. Při vytvoření webhooku Emailit vygeneruje podpisový klíč pro daný endpoint
  2. Pro každé doručení Emailit spojí časové razítko a surové tělo požadavku jako {timestamp}.{rawBody}
  3. Nad tímto řetězcem se vypočítá HMAC-SHA256 hash pomocí vašeho podpisového klíče
  4. Podpis se odešle v hlavičce X-Emailit-Signature a časové razítko v X-Emailit-Timestamp
  5. Váš server znovu vypočítá podpis a porovná ho s hodnotou v hlavičce

Ověření podpisu

Pro ověření webhook podpisu:

  1. Extrahujte hlavičky X-Emailit-Signature a X-Emailit-Timestamp z požadavku
  2. Spojte časové razítko a surové tělo požadavku jako {timestamp}.{rawBody}
  3. Vypočítejte HMAC-SHA256 hash tohoto řetězce pomocí vašeho webhook podpisového klíče
  4. Porovnejte vypočítaný hash s podpisem pomocí časově bezpečného porovnání
  5. Volitelně odmítněte požadavky, kde je časové razítko starší než několik minut, abyste se ochránili před replay útoky

Ověření podpisu

import crypto from 'crypto';

function verifyWebhookSignature(rawBody, signature, timestamp, secret) {
  const signedPayload = `${timestamp}.${rawBody}`;
  const computed = crypto
    .createHmac('sha256', secret)
    .update(signedPayload, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(computed, 'hex'),
    Buffer.from(signature, 'hex')
  );
}

// Ve vašem webhook handleru:
const rawBody = req.body; // surové tělo požadavku jako string
const signature = req.headers['x-emailit-signature'];
const timestamp = req.headers['x-emailit-timestamp'];
const secret = process.env.WEBHOOK_SIGNING_SECRET;

// Ochrana před replay útoky (5minutová tolerance)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
  return res.status(401).send('Požadavek je příliš starý');
}

if (!verifyWebhookSignature(rawBody, signature, timestamp, secret)) {
  return res.status(401).send('Neplatný podpis');
}

Důležité poznámky

  • Podpis se počítá nad {timestamp}.{rawBody} — vždy používejte surový řetězec těla požadavku, ne znovu serializovanou verzi (např. JSON.stringify(req.body)). Rozdíly v bílých znacích nebo pořadí klíčů podpis poruší.
  • Vždy používejte časově bezpečnou porovnávací funkci (např. crypto.timingSafeEqual, hmac.compare_digest, hash_equals) k prevenci timing útoků.
  • Zvažte odmítnutí požadavků, kde je X-Emailit-Timestamp starší než několik minut, abyste se ochránili před replay útoky.
  • Udržujte váš podpisový klíč v bezpečí — ukládejte ho do proměnných prostředí, ne do zdrojového kódu.
  • Váš webhook podpisový klíč najdete v Emailit dashboardu v nastavení webhooku nebo přes Webhooks API.