Aller au contenu principal

Sécurité des webhooks

La sécurité des webhooks est essentielle pour garantir que les requêtes proviennent bien de moncreneau.

Signature HMAC-SHA256

Chaque webhook est signé avec HMAC-SHA256. Le header X-Moncreneau-Signature contient la signature :

X-Moncreneau-Signature: sha256=5d41402abc4b2a76b9719d911017c592

Vérification de la signature

Étape 1 : Récupérer le secret

Lors de la création du webhook, vous recevez un secret :

whsec_abc123def456ghi789jkl012mno345

⚠️ Conservez ce secret en sécurité ! Ne le commitez jamais dans votre code.

Étape 2 : Calculer la signature

Calculez le HMAC du payload avec votre secret :

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
// Extraire la signature (enlever le préfixe "sha256=")
const receivedSignature = signature.replace('sha256=', '');

// Calculer la signature attendue
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expectedSignature = hmac.digest('hex');

// Comparaison sécurisée
return crypto.timingSafeEqual(
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}

// Utilisation avec Express
app.post('/webhooks/moncreneau', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-moncreneau-signature'];
const secret = process.env.MONCRENEAU_WEBHOOK_SECRET;

if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}

// Maintenant on peut parser le JSON en toute sécurité
const event = JSON.parse(req.body.toString());
handleEvent(event);

res.status(200).send('OK');
});

Java (Spring Boot)

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;

@RestController
@RequestMapping("/webhooks")
public class WebhookController {

@Value("${moncreneau.webhook.secret}")
private String webhookSecret;

@PostMapping("/moncreneau")
public ResponseEntity<String> handleWebhook(
@RequestBody String payload,
@RequestHeader("X-Moncreneau-Signature") String signature) {

if (!verifySignature(payload, signature)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid signature");
}

// Parser et traiter l'événement
ObjectMapper mapper = new ObjectMapper();
WebhookEvent event = mapper.readValue(payload, WebhookEvent.class);
handleEvent(event);

return ResponseEntity.ok("OK");
}

private boolean verifySignature(String payload, String signature) {
try {
// Enlever "sha256="
String receivedSig = signature.replace("sha256=", "");

// Calculer HMAC
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(
webhookSecret.getBytes("UTF-8"),
"HmacSHA256"
);
hmac.init(key);

byte[] hash = hmac.doFinal(payload.getBytes("UTF-8"));
String expectedSig = bytesToHex(hash);

// Comparaison sécurisée
return MessageDigest.isEqual(
receivedSig.getBytes(),
expectedSig.getBytes()
);
} catch (Exception e) {
return false;
}
}

private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}

PHP (Laravel)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class WebhookController extends Controller
{
public function handleMoncreneau(Request $request)
{
$signature = $request->header('X-Moncreneau-Signature');
$payload = $request->getContent();
$secret = config('moncreneau.webhook_secret');

if (!$this->verifySignature($payload, $signature, $secret)) {
return response('Invalid signature', 401);
}

$event = json_decode($payload, true);
$this->handleEvent($event);

return response('OK', 200);
}

private function verifySignature($payload, $signature, $secret)
{
// Enlever "sha256="
$receivedSignature = str_replace('sha256=', '', $signature);

// Calculer la signature attendue
$expectedSignature = hash_hmac('sha256', $payload, $secret);

// Comparaison sécurisée
return hash_equals($expectedSignature, $receivedSignature);
}
}

Python (Django/Flask)

import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Vérifie la signature HMAC d'un webhook."""
# Enlever "sha256="
received_signature = signature.replace('sha256=', '')

# Calculer la signature attendue
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()

# Comparaison sécurisée
return hmac.compare_digest(expected_signature, received_signature)

# Flask
from flask import Flask, request

@app.route('/webhooks/moncreneau', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Moncreneau-Signature')
payload = request.get_data()
secret = os.getenv('MONCRENEAU_WEBHOOK_SECRET')

if not verify_webhook_signature(payload, signature, secret):
return 'Invalid signature', 401

event = request.get_json()
handle_event(event)

return 'OK', 200

# Django
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def webhook_view(request):
if request.method == 'POST':
signature = request.META.get('HTTP_X_MONCRENEAU_SIGNATURE')
payload = request.body
secret = settings.MONCRENEAU_WEBHOOK_SECRET

if not verify_webhook_signature(payload, signature, secret):
return HttpResponse('Invalid signature', status=401)

event = json.loads(payload)
handle_event(event)

return HttpResponse('OK', status=200)

Bonnes pratiques

✅ À faire

  1. Toujours vérifier la signature avant de traiter le payload
  2. Utiliser une comparaison timing-safe pour éviter les timing attacks
  3. Stocker le secret dans les variables d'environnement
  4. Logger les signatures invalides pour détecter les tentatives d'attaque
  5. Utiliser HTTPS en production (obligatoire)

❌ À éviter

  1. Ne jamais skipper la vérification de signature
  2. Ne jamais comparer avec == (vulnérable aux timing attacks)
  3. Ne jamais logger le secret webhook
  4. Ne jamais utiliser HTTP en production

Rotation du secret

Si votre secret est compromis :

  1. Générez un nouveau secret dans Dashboard → Webhooks
  2. Mettez à jour votre code avec le nouveau secret
  3. Déployez la nouvelle version
  4. Désactivez l'ancien secret

IP Whitelisting (optionnel)

Pour plus de sécurité, vous pouvez restreindre les requêtes aux IPs de moncreneau :

# IPs moncreneau (à confirmer avec le support)
41.223.45.0/24
41.223.46.0/24

Configuration Nginx :

location /webhooks/moncreneau {
allow 41.223.45.0/24;
allow 41.223.46.0/24;
deny all;

proxy_pass http://localhost:3000;
}

Tests

Générer une signature de test

const crypto = require('crypto');

const payload = JSON.stringify({
id: "evt_test123",
type: "appointment.created",
data: { ... }
});

const secret = "whsec_test_secret";
const signature = "sha256=" + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

console.log(signature);
// sha256=5d41402abc4b2a76b9719d911017c592

Tester avec curl

curl -X POST http://localhost:3000/webhooks/moncreneau \
-H "Content-Type: application/json" \
-H "X-Moncreneau-Signature: sha256=5d41402abc4b2a76b9719d911017c592" \
-d '{"id":"evt_test","type":"appointment.created","data":{}}'

Prochaines étapes