Aller au contenu principal

Vérifier la signature des webhooks

N'importe qui peut envoyer une requête à votre adresse de réception : seule la signature prouve qu'un webhook vient d'Integral Pay et n'a pas été modifié. Vérifiez-la avant toute autre action, et refusez tout webhook dont la signature est invalide ou l'horodatage trop ancien.

En-têtes envoyés​

En-têteContenu
X-Signature-SHA256Signature HMAC-SHA256, en hexadécimal (64 caractères)
X-Webhook-TimestampHeure d'envoi, en secondes Unix (ex. 1762425330)
X-Webhook-IDIdentifiant de l'événement, identique pour toutes les tentatives
X-Webhook-DeliveryIdentifiant de cette tentative d'envoi
X-Webhook-EventType d'événement : payment.updated
User-AgentIntegralPay-Webhooks/1.0

Calcul de la signature​

Le texte signé est l'horodatage, un point, puis le corps brut de la requête, octet pour octet. La clé est votre secret de webhook.

signature = hex( HMAC-SHA256( secret, X-Webhook-Timestamp + "." + corps_brut ) )
  1. Lisez le corps brut, avant tout décodage JSON. Un JSON décodé puis réencodé change d'octets (espaces, ordre des clés, caractères accentués) et la signature ne correspond plus.
  2. Contrôlez l'horodatage : refusez le webhook s'il diffère de plus de 5 minutes de votre heure. Un webhook intercepté ne pourra pas être rejoué plus tard. Gardez l'horloge de votre serveur synchronisée (NTP).
  3. Comparez à temps constant la signature calculée et X-Signature-SHA256 : hmac.compare_digest, hash_equals, crypto.timingSafeEqual. Jamais ==.
  4. Seulement ensuite, décodez le JSON et traitez l'événement.

Exemples​

webhooks.py
import hashlib
import hmac
import json
import os
import time

from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

WEBHOOK_SECRET = os.environ["INTEGRALPAY_WEBHOOK_SECRET"].encode()
TOLERANCE_SECONDS = 300


def signature_valide(timestamp: str, signature: str, body: bytes) -> bool:
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
attendue = hmac.new(WEBHOOK_SECRET, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(attendue, signature)


@csrf_exempt
@require_POST
def integralpay_webhook(request):
if not signature_valide(
request.headers.get("X-Webhook-Timestamp", ""),
request.headers.get("X-Signature-SHA256", ""),
request.body, # corps brut, en octets
):
return HttpResponseBadRequest("Signature invalide")

event = json.loads(request.body)
# ... traitement : voir « Idempotence »
return HttpResponse(status=200)

Si la vérification échoue​

Contrôlez dans l'ordre :

  1. Le secret : celui de cette adresse de réception (chaque adresse a le sien ; le secret de test diffère de celui de production), sans espace ni retour à la ligne en trop, et pas l'ancien si vous l'avez renouvelé.
  2. Le corps brut : aucun middleware (express.json(), bodyParser, un framework qui décode le JSON) ne doit le transformer avant vous.
  3. L'horloge de votre serveur, si seuls les contrôles d'horodatage échouent.