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​