Webhook HMAC
Верификация подлинности входящих Webhook-запросов через HMAC-SHA256 подпись.
Как работает
При каждом Webhook-запросе FormHook добавляет заголовок X-FormHook-Signature.
Значение — HMAC-SHA256 от тела запроса, вычисленный с вашим Secret.
Проверяйте подпись на своём сервере перед обработкой заявки.
Настройка Secret
Дашборд → Сайт → Настройки → Каналы → Webhook → поле Secret.
Укажите произвольную строку длиной от 16 символов. Secret хранится в зашифрованном виде.
Формат заголовка
X-FormHook-Signature: sha256=abc123def456... Алгоритм: HMAC-SHA256(raw_body, secret), hex-encoded с префиксом sha256=.
Проверка подписи
Node.js
import crypto from 'crypto';
function verifyFormhookSignature(
rawBody: Buffer | string,
signature: string,
secret: string,
): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}
// Express пример
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-formhook-signature'] as string;
if (!verifyFormhookSignature(req.body, signature, process.env.FORMHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString());
console.log('New submission:', payload.submission.id);
res.json({ ok: true });
}); Python
import hmac
import hashlib
def verify_formhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# FastAPI пример
@app.post('/webhook')
async def webhook(request: Request):
raw_body = await request.body()
signature = request.headers.get('x-formhook-signature', '')
if not verify_formhook_signature(raw_body, signature, FORMHOOK_SECRET):
raise HTTPException(status_code=401, detail='Invalid signature')
payload = await request.json()
print('New submission:', payload['submission']['id'])
return {'ok': True} Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func verifyFormhookSignature(rawBody []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-FormHook-Signature")
if !verifyFormhookSignature(body, sig, os.Getenv("FORMHOOK_SECRET")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// обрабатывайте payload...
} timingSafeEqual, hmac.compare_digest, hmac.Equal) — обычное сравнение строк уязвимо к timing-атакам.
Тестирование без сервера
Используйте webhook.site для проверки входящих запросов и заголовков во время разработки.