Skip to main content

Pourquoi vérifier les signatures ?

N’importe qui peut envoyer une requête POST à votre endpoint webhook. La signature HMAC-SHA256 garantit que le payload vient bien de DLoopIQ et qu’il n’a pas été modifié en transit.

Comment ça marche

  1. Vous configurez un secret lors de la création du webhook
  2. DLoopIQ calcule HMAC-SHA256(payload, secret) pour chaque requête
  3. Le résultat est envoyé dans le header x-dloopiq-signature: sha256=<hash>
  4. Vous recalculez le HMAC côté serveur et comparez

Implémentation

Node.js / Express

import crypto from 'crypto'
import express from 'express'

const WEBHOOK_SECRET = process.env.DLOOPIQ_WEBHOOK_SECRET!

function verifySignature(
  payload: Buffer,
  signatureHeader: string,
  secret: string
): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')

  // Comparaison en temps constant pour éviter les timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  )
}

app.post(
  '/webhook/dloopiq',
  express.raw({ type: 'application/json' }),  // ← IMPORTANT : raw body
  (req, res) => {
    const signature = req.headers['x-dloopiq-signature'] as string

    if (!signature) {
      return res.status(401).json({ error: 'Signature manquante' })
    }

    if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
      return res.status(401).json({ error: 'Signature invalide' })
    }

    const event = JSON.parse(req.body.toString())
    
    // Traiter l'événement de manière asynchrone
    processEvent(event).catch(console.error)
    
    // Répondre immédiatement
    res.status(200).json({ received: true })
  }
)
Utilisez express.raw() et non express.json() pour parser le body. Le HMAC est calculé sur les bytes bruts — si vous parsez en JSON puis re-sérialisez, la signature ne correspondra plus.

Python / FastAPI

import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException

WEBHOOK_SECRET = os.environ["DLOOPIQ_WEBHOOK_SECRET"]

@app.post("/webhook/dloopiq")
async def webhook(request: Request):
    body = await request.body()
    signature = request.headers.get("x-dloopiq-signature", "")

    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="Signature invalide")

    event = json.loads(body)
    # traiter...
    return {"received": True}

PHP

<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_DLOOPIQ_SIGNATURE'] ?? '';
$secret = getenv('DLOOPIQ_WEBHOOK_SECRET');

$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    die(json_encode(['error' => 'Signature invalide']));
}

$event = json_decode($payload, true);
// traiter...
echo json_encode(['received' => true]);

Tester votre endpoint

Générez une signature de test en local :
# Simuler un webhook DLoopIQ
SECRET="votre_secret"
PAYLOAD='{"event":"task.completed","taskId":"test123","result":"Positif"}'

SIGNATURE="sha256=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)"

curl -X POST https://votre-serveur.com/webhook/dloopiq \
  -H "Content-Type: application/json" \
  -H "x-dloopiq-signature: $SIGNATURE" \
  -d "$PAYLOAD"

Idempotence

En cas de retry (timeout ou 5xx), DLoopIQ peut envoyer le même événement plusieurs fois. Protégez-vous contre les doublons :
// Stocker les taskId déjà traités (Redis, DB...)
async function processEvent(event: any) {
  if (event.event === 'task.completed') {
    const alreadyProcessed = await redis.get(`webhook:${event.taskId}`)
    if (alreadyProcessed) return  // ignorer le doublon

    await redis.set(`webhook:${event.taskId}`, '1', 'EX', 86400)
    
    // Traiter...
    await saveResultToDatabase(event)
  }
}

Checklist sécurité

  • ✅ Vérifier x-dloopiq-signature sur chaque requête
  • ✅ Utiliser timingSafeEqual / hash_equals (pas ===)
  • ✅ Parser le body en raw bytes, pas en JSON avant vérification
  • ✅ Répondre 200 avant de traiter (éviter les timeouts)
  • ✅ Gérer l’idempotence (même événement reçu 2 fois)
  • ✅ Stocker DLOOPIQ_WEBHOOK_SECRET dans les variables d’environnement