Vai al contenuto
Docs

Guida pratica

Verifica le firme dei webhook

Controlla che le richieste webhook arrivino da Emailit verificando la firma HMAC-SHA256 e il timestamp, con codice per Node.js, Python, PHP, Ruby, Go e Laravel.

Aggiornato il 1 ott 2026

Chiunque conosca l’URL del tuo webhook può inviargli richieste. Ogni richiesta di Emailit è firmata con il secret del webhook, quindi verificare la firma dimostra che la richiesta proviene da Emailit e non è stata modificata lungo il percorso. Controllare il timestamp impedisce inoltre che vecchie richieste vengano riprodotte (replay). Fai entrambe le cose prima di agire su qualsiasi evento.

Come funziona la firma

Ogni webhook ha il proprio secret: whsec_ seguito da 64 caratteri esadecimali. Lo vedi una sola volta, quando crei il webhook o reimposti il secret, e Recupera un webhook lo restituisce.

Per ogni richiesta, Emailit:

  1. Prende l’ora Unix attuale in secondi, ad esempio 1790846045, e la invia in X-Emailit-Timestamp.
  2. Costruisce la stringa {timestamp}.{raw body}: il timestamp, un punto e i byte esatti del corpo della richiesta.
  3. Calcola l’HMAC-SHA256 di questa stringa, usando come chiave il secret completo, compreso whsec_.
  4. Invia il risultato, codificato in esadecimale minuscolo, in X-Emailit-Signature.

Per verificare, il tuo endpoint ripete i passaggi 2 e 3 con il corpo grezzo ricevuto e confronta il risultato con l’header usando un confronto a tempo costante. Poi controlla che il timestamp sia recente. Ti consigliamo una tolleranza di 5 minuti (300 secondi). Ogni nuovo tentativo viene firmato di nuovo con un timestamp aggiornato, quindi una finestra di 5 minuti non rifiuta i nuovi tentativi.

Verifica le richieste

Ogni esempio legge il corpo grezzo, rifiuta con 401 le richieste con una firma mancante, scaduta o errata, e solo dopo analizza l’array 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);

In Laravel, le route in routes/api.php non vengono controllate per i token CSRF e ricevono il prefisso /api, quindi l’URL dell’esempio è https://acme.com/api/webhooks/emailit. Se registri la route in routes/web.php, escludi il percorso dalla verifica CSRF, altrimenti Laravel rifiuta le richieste di Emailit con 419.

Testa la verifica

Prima del deploy, controlla la tua funzione con questo esempio noto. Chiama direttamente la parte HMAC, perché il timestamp è nel passato e non supera il controllo dei 5 minuti.

Dato Valore
Secret whsec_4f2a9c1e7b3d5a8f0e6c2b9d4a7f1e3c5b8d0a2f6e9c3b7d1a5f8e2c4b6d9a0f
Timestamp 1790846045
Firma attesa 7242b580f77ec488f8ac21b57f092e3db61a431694f13074c7b4c85a4dd04be3

Corpo grezzo, esattamente come mostrato, senza a capo finale:

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

Poi usa Send test sul webhook per confermare che l’endpoint in produzione accetta una vera richiesta firmata. Vedi Configura un webhook.

Errori comuni

  • Analizzare prima di verificare. I framework che analizzano il JSON in automatico (express.json(), request.json in Flask, i params di Rails) ti danno un oggetto, non i byte che Emailit ha firmato. Leggi il corpo grezzo nella route del webhook.
  • Serializzare di nuovo il JSON. JSON.stringify(req.body) o json.dumps(data) producono spazi, ordine delle chiavi o escape diversi, quindi l’HMAC non corrisponde. Calcola sempre l’hash del corpo esattamente come lo hai ricevuto.
  • Modificare il secret. Usa il valore intero, compreso whsec_, come testo semplice. Non togliere il prefisso e non decodificarlo da esadecimale o Base64.
  • Firmare la stringa sbagliata. L’input è il timestamp, un punto, poi il corpo. Se firmi solo il corpo, o unisci le parti con un altro separatore, la verifica non riesce.
  • Usare == per il confronto. Usa una funzione a tempo costante come crypto.timingSafeEqual, hmac.compare_digest, hash_equals, Rack::Utils.secure_compare o hmac.Equal.
  • Un orologio del server sbagliato. Se l’orologio del server si sfasa, le richieste valide cadono fuori dalla tolleranza. Tienilo sincronizzato con NTP.
  • Un secret vecchio o non corrispondente. Dopo Reset, il vecchio secret smette subito di funzionare, anche per i nuovi tentativi. Ogni webhook ha il proprio secret, quindi due endpoint non possono condividerne uno.
  • Middleware che riscrive il corpo. Proxy, API gateway o la decompressione del corpo che modificano i byte prima che il tuo codice li veda invalidano la firma. Verifica il più vicino possibile alla richiesta grezza.

Le richieste che non superano la verifica ricevono un 401 dal tuo endpoint e vengono ritentate come qualsiasi altro errore, quindi se correggi un secret configurato male entro poche ore non perdi nulla.

Questa pagina ti è stata utile?

Grazie del feedback.

Grazie, leggiamo ogni messaggio.