Aller au contenu
Docs

Guide pratique

Vérifier les signatures de webhook

Vérifiez que les requêtes de webhook proviennent d’Emailit en contrôlant la signature HMAC-SHA256 et l’horodatage, avec du code pour Node.js, Python, PHP, Ruby, Go et Laravel.

Mis à jour le 1 oct. 2026

Toute personne qui connaît l’URL de votre webhook peut lui envoyer des requêtes. Chaque requête d’Emailit est signée avec le secret de votre webhook : vérifier la signature prouve que la requête provient d’Emailit et n’a pas été modifiée en chemin. Le contrôle de l’horodatage empêche en outre le rejeu d’anciennes requêtes. Faites les deux avant d’agir sur un événement.

Fonctionnement de la signature

Chaque webhook a son propre secret : whsec_ suivi de 64 caractères hexadécimaux. Il vous est affiché une seule fois, lorsque vous créez le webhook ou réinitialisez le secret, et Récupérer un webhook le renvoie.

Pour chaque requête, Emailit :

  1. Prend l’horodatage Unix actuel en secondes, par exemple 1790846045, et l’envoie dans X-Emailit-Timestamp.
  2. Construit la chaîne {timestamp}.{raw body} : l’horodatage, un point, puis les octets exacts du corps de la requête.
  3. Calcule le HMAC-SHA256 de cette chaîne, avec le secret complet, whsec_ compris, comme clé.
  4. Envoie le résultat, encodé en hexadécimal minuscule, dans X-Emailit-Signature.

Pour vérifier la requête, votre endpoint répète les étapes 2 et 3 avec le corps brut reçu et compare son résultat à l’en-tête au moyen d’une comparaison à temps constant. Il vérifie ensuite que l’horodatage est récent. Nous recommandons une tolérance de 5 minutes (300 secondes). Chaque nouvelle tentative est signée de nouveau avec un horodatage à jour : une fenêtre de 5 minutes ne rejette donc pas les nouvelles tentatives.

Vérifier les requêtes

Chaque exemple lit le corps brut, rejette avec 401 les requêtes dont la signature est absente, expirée ou incorrecte, et seulement ensuite analyse le tableau 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);

Dans Laravel, les routes de routes/api.php ne sont pas soumises au contrôle des jetons CSRF et reçoivent le préfixe /api : l’URL ci-dessus est donc https://acme.com/api/webhooks/emailit. Si vous enregistrez la route dans routes/web.php, excluez le chemin de la vérification CSRF, sinon Laravel rejette les requêtes d’Emailit avec 419.

Tester votre vérification

Testez votre fonction avec cet exemple connu avant de déployer. Appelez directement la partie HMAC, car l’horodatage est dans le passé et échoue au contrôle des 5 minutes.

Entrée Valeur
Secret whsec_4f2a9c1e7b3d5a8f0e6c2b9d4a7f1e3c5b8d0a2f6e9c3b7d1a5f8e2c4b6d9a0f
Horodatage 1790846045
Signature attendue 7242b580f77ec488f8ac21b57f092e3db61a431694f13074c7b4c85a4dd04be3

Corps brut, exactement tel qu’affiché, sans saut de ligne final :

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

Utilisez ensuite Send test sur le webhook pour confirmer que votre endpoint déployé accepte une vraie requête signée. Consultez Configurer un webhook.

Erreurs fréquentes

  • Analyser avant de vérifier. Les frameworks qui analysent automatiquement le JSON (express.json(), request.json dans Flask, les params de Rails) vous donnent un objet, pas les octets signés par Emailit. Lisez le corps brut sur la route du webhook.
  • Resérialiser le JSON. JSON.stringify(req.body) ou json.dumps(data) produit des espaces, un ordre de clés ou un échappement différents : le HMAC ne correspondra pas. Hachez toujours le corps exactement tel que vous l’avez reçu.
  • Modifier le secret. Utilisez la valeur complète, whsec_ compris, en texte brut. Ne retirez pas le préfixe et ne le décodez pas depuis l’hexadécimal ou le Base64.
  • Signer la mauvaise chaîne. L’entrée est l’horodatage, un point, puis le corps. Signer uniquement le corps, ou utiliser un autre séparateur, échoue.
  • Comparer avec ==. Utilisez une fonction à temps constant comme crypto.timingSafeEqual, hmac.compare_digest, hash_equals, Rack::Utils.secure_compare ou hmac.Equal.
  • Une horloge serveur inexacte. Si l’horloge de votre serveur dérive, des requêtes valides sortent de la tolérance. Synchronisez-la avec NTP.
  • Un secret ancien ou qui ne correspond pas. Après Reset, l’ancien secret cesse immédiatement de fonctionner, y compris pour les nouvelles tentatives. Chaque webhook a son propre secret : deux endpoints ne peuvent pas en partager un.
  • Un middleware qui réécrit le corps. Les proxys, les passerelles d’API ou la décompression du corps qui modifient les octets avant que votre code ne les voie cassent la signature. Vérifiez la signature au plus près de la requête brute.

Les requêtes dont la vérification échoue reçoivent un 401 de votre endpoint et font l’objet de nouvelles tentatives comme tout autre échec : si vous corrigez un secret mal configuré en quelques heures, vous ne perdez rien.

Cette page vous a-t-elle été utile ?

Merci pour votre retour.

Merci, nous lisons chaque message.