webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
| Header |
Was es ist |
| webhook-id |
Die Zustellung, derselbe Wert wie delivery_id im Body. Merken Sie sich die Zustellungen, die Sie bereits verarbeitet haben, und antworten Sie auf eine Wiederholung mit 200, ohne die Arbeit noch einmal zu machen. |
| webhook-timestamp |
Der Zeitpunkt der Signatur, in vollen Sekunden seit 1970. Lehnen Sie alles ab, was mehr als fünf Minuten von Ihrer eigenen Uhr abweicht, damit eine alte Zustellung nicht noch einmal an Sie gesendet werden kann. |
| webhook-signature |
v1, dann das Base64-HMAC-SHA256 aus der ID, dem Zeitstempel und dem Rohtext des Bodys, mit Punkten verbunden und mit Ihrem Signaturschlüssel erstellt. Es kann mehr als eines geben, durch Leerzeichen getrennt. Akzeptieren Sie die Zustellung, wenn eines davon passt. |
Der Signaturschlüssel
Er beginnt mit whsec_. Entfernen Sie das und decodieren Sie den Rest mit Base64: Diese Bytes sind der HMAC-Schlüssel, nicht der Text, den Sie kopiert haben. Er wird einmal angezeigt, wenn Sie den Webhook verbinden oder auf der Publishing-Seite auf "New signing key" klicken, und wir senden ihn nie.
Prüfen Sie den Rohtext des Bodys
Berechnen Sie die Signatur aus den Bytes genau so, wie sie angekommen sind, bevor irgendetwas sie verarbeitet. JSON, das decodiert und erneut codiert wurde, besteht nicht aus denselben Bytes, und seine Signatur passt nicht.
PHP
<?php
$raw = getenv('SEONIS_SIGNING_KEY'); // whsec_...
// No key, no deliveries. A missing or empty variable would otherwise become
// an empty HMAC key, and anybody can sign with that.
$secret = is_string($raw) && str_starts_with($raw, 'whsec_')
? base64_decode(substr($raw, strlen('whsec_')), true)
: false;
if ($secret === false || strlen($secret) < 24) {
http_response_code(500);
exit('Signing key not configured.');
}
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$header = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
if (! ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit('Too old.');
}
$expected = base64_encode(hash_hmac('sha256', "$id.$timestamp.$body", $secret, true));
$valid = false;
foreach (explode(' ', $header) as $signature) {
[$version, $value] = array_pad(explode(',', $signature, 2), 2, '');
$valid = $valid || ($version === 'v1' && hash_equals($expected, $value));
}
if (! $valid) {
http_response_code(401);
exit('Bad signature.');
}
$delivery = json_decode($body, true);
Node.js
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// No key, no server. An empty variable would otherwise become an empty HMAC
// key, and anybody can sign with that.
const raw = process.env.SEONIS_SIGNING_KEY ?? '';
if (!raw.startsWith('whsec_')) throw new Error('SEONIS_SIGNING_KEY is not set.');
const key = Buffer.from(raw.slice('whsec_'.length), 'base64');
if (key.length < 24) throw new Error('SEONIS_SIGNING_KEY is not a valid key.');
// express.raw keeps the body as the bytes we signed.
app.post('/hooks/seonis', express.raw({ type: 'application/json' }), (req, res) => {
const id = req.get('webhook-id') ?? '';
const timestamp = req.get('webhook-timestamp') ?? '';
const header = req.get('webhook-signature') ?? '';
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).send('Too old.');
}
const expected = crypto.createHmac('sha256', key)
.update(`${id}.${timestamp}.`)
.update(req.body)
.digest();
const valid = header.split(' ').some((signature) => {
const [version, value = ''] = signature.split(',');
const given = Buffer.from(value, 'base64');
return version === 'v1' && given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
if (!valid) return res.status(401).send('Bad signature.');
const delivery = JSON.parse(req.body);
res.json({ ok: true });
});
Oder verwenden Sie eine Bibliothek
Die offiziellen Standard Webhooks-Bibliotheken für PHP, JavaScript, Python, Go, Ruby und mehr erledigen all das mit einem einzigen Aufruf. Geben Sie ihnen den Signaturschlüssel genau so, wie er Ihnen angezeigt wurde, einschließlich whsec_.
github.com/standard-webhooks
Die älteren Header
Zustellungen enthalten auch das Signaturgeheimnis als Bearer-Token und X-AutoSEO-LT-Signature, ein HMAC nur über den Body, das mit diesem Geheimnis erstellt wird. Sie werden weiter gesendet, damit Endpunkte, die darauf aufbauen, weiter funktionieren. Ein neuer Endpunkt sollte stattdessen webhook-signature prüfen: Die alte Signatur enthält weder Zeit noch ID, und ihr Schlüssel wird bei jeder Anfrage mitgesendet.
Die Formate AutoSEO, Outrank und BabyLoveGrowth senden genau das, was diese Dienste gesendet haben, und enthalten diese Header daher nicht.