View Categories

Vérifier la signature des notifications de rendez-vous

Apenday signe les notifications envoyées à votre système. La signature vous permet de vérifier que l’appel vient bien d’Apenday et que son contenu n’a pas été modifié en chemin.

Récupérer le secret #

Le secret est propre à chaque webhook et n’est affiché qu’une seule fois.

  1. Depuis l’onglet Avancé de votre profil utilisateur, cliquez sur Ajouter un webhook.
  2. Après l’enregistrement, la fenêtre Secret de signature du webhook affiche le secret.
  3. Copiez-le, enregistrez-le dans votre système, puis cliquez sur J’ai conservé ce secret.

Le secret est une chaîne de 64 caractères hexadécimaux. Il n’est plus consultable une fois la fenêtre fermée. Par l’API, il est renvoyé dans la réponse de la souscription, voir Utiliser les webhooks Apenday.

Les en-têtes #

En-têteContenu
X-Apenday-TimestampHeure de la tentative d’envoi, en secondes écoulées depuis le 1er janvier 1970, en UTC. Exemple : 1789480000
X-Apenday-Event-IdIdentifiant unique de l’événement. Exemple : 3f6c1a2e-8d4b-4c7e-9a51-0b2e7d9c4f10
X-Apenday-EventL’événement notifié. Exemple : meeting_confirmed
X-Apenday-Event-TimestampMoment de l’événement, en secondes écoulées depuis le 1er janvier 1970, en UTC. Exemple : 1789479700
X-Apenday-SignatureLe préfixe v1= suivi de la signature, en hexadécimal minuscule. Exemple : v1=3a5f1c9e...

X-Apenday-Timestamp et X-Apenday-Signature ne sont présents que sur un webhook signé. Les trois autres sont présents sur toutes les notifications.

La chaîne signée #

La signature est un HMAC-SHA256 calculé sur les valeurs de quatre en-têtes puis le corps de la requête, séparés par un point, dans cet ordre :

{X-Apenday-Timestamp}.{X-Apenday-Event-Id}.{X-Apenday-Event}.{X-Apenday-Event-Timestamp}.{corps}

Exemple :

1789480000.3f6c1a2e-8d4b-4c7e-9a51-0b2e7d9c4f10.meeting_confirmed.1789479700.{"@context":"/api/contexts/Appointment","@id":"/api/public/appointments/...

Trois points font échouer la plupart des premières intégrations.

Signez le corps brut. Si votre code désérialise le corps puis le resérialise avant de calculer la signature, vous obtenez une valeur différente. Lisez le corps tel qu’il arrive, et reprenez les valeurs des en-têtes telles que reçues.

Utilisez le secret tel quel. C’est une chaîne hexadécimale, mais elle sert de clé sous sa forme texte. Ne la convertissez pas en binaire avant de calculer le HMAC.

Comparez en temps constant, avec hash_equals en PHP ou crypto.timingSafeEqual en JavaScript. Jamais == ni ===.

Vérifier en PHP #

$secret = 'votre-secret';

$timestamp = $_SERVER['HTTP_X_APENDAY_TIMESTAMP'] ?? '';
$eventId = $_SERVER['HTTP_X_APENDAY_EVENT_ID'] ?? '';
$event = $_SERVER['HTTP_X_APENDAY_EVENT'] ?? '';
$eventTimestamp = $_SERVER['HTTP_X_APENDAY_EVENT_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_APENDAY_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');

// Rejeter un appel trop ancien. Cinq minutes est une tolérance raisonnable.
if ('' === $timestamp || abs(time() - (int) $timestamp) > 300) {
    http_response_code(403);
    exit;
}

$signed = implode('.', [$timestamp, $eventId, $event, $eventTimestamp, $body]);
$expected = 'v1=' . hash_hmac('sha256', $signed, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(403);
    exit;
}

// La notification est authentique.
$appointment = json_decode($body, true);

http_response_code(200);

Vérifier en JavaScript #

Avec Express, express.json() a déjà consommé et reformaté le corps au moment où votre code s’exécute. Conservez le corps brut avec l’option verify.

const crypto = require('crypto');
const express = require('express');

const SECRET = 'votre-secret';
const app = express();

app.use(express.json({
    verify: (req, res, buf) => {
        req.rawBody = buf.toString('utf8');
    },
}));

app.post('/apenday/webhook', (req, res) => {
    const timestamp = req.get('X-Apenday-Timestamp') || '';
    const eventId = req.get('X-Apenday-Event-Id') || '';
    const event = req.get('X-Apenday-Event') || '';
    const eventTimestamp = req.get('X-Apenday-Event-Timestamp') || '';
    const signature = req.get('X-Apenday-Signature') || '';

    // Rejeter un appel trop ancien. Cinq minutes est une tolérance raisonnable.
    if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
        return res.sendStatus(403);
    }

    const signed = [timestamp, eventId, event, eventTimestamp, req.rawBody].join('.');
    const expected = 'v1=' + crypto
        .createHmac('sha256', SECRET)
        .update(signed)
        .digest('hex');

    const received = Buffer.from(signature);
    const computed = Buffer.from(expected);

    if (received.length !== computed.length || !crypto.timingSafeEqual(received, computed)) {
        return res.sendStatus(403);
    }

    // La notification est authentique.
    res.sendStatus(200);
});

crypto.timingSafeEqual lève une exception si les deux valeurs n’ont pas la même longueur. La comparaison des longueurs avant l’appel n’est donc pas optionnelle.

En cas de perte du secret #

Ouvrez le webhook et cliquez sur Régénérer le secret. L’ancien secret cesse immédiatement d’être valable : installez le nouveau dans votre système avant de reprendre la vérification.

Ce que votre système doit prévoir #

Une relance porte son propre horodatage. X-Apenday-Timestamp et la signature sont recalculés à chaque tentative. Une relance peut arriver une trentaine de minutes après l’événement, mais votre contrôle d’ancienneté n’a pas à être élargi pour l’accepter.

Les en-têtes d’événement ne changent pas d’une tentative à l’autre. X-Apenday-Event-Id et X-Apenday-Event-Timestamp servent à dédoublonner et à ordonner les notifications, comme décrit dans Utiliser les webhooks Apenday, section « Répondre à une notification ».

Powered by BetterDocs