Конфигурация

Webhook HMAC

Верификация подлинности входящих Webhook-запросов через HMAC-SHA256 подпись.

Как работает

При каждом Webhook-запросе FormHook добавляет заголовок X-FormHook-Signature. Значение — HMAC-SHA256 от тела запроса, вычисленный с вашим Secret. Проверяйте подпись на своём сервере перед обработкой заявки.

Настройка Secret

Дашборд → Сайт → Настройки → Каналы → Webhook → поле Secret.

Укажите произвольную строку длиной от 16 символов. Secret хранится в зашифрованном виде.

Формат заголовка

bash
X-FormHook-Signature: sha256=abc123def456...

Алгоритм: HMAC-SHA256(raw_body, secret), hex-encoded с префиксом sha256=.

Проверка подписи

Node.js

typescript
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

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

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...
}
Используйте timing-safe comparison (timingSafeEqual, hmac.compare_digest, hmac.Equal) — обычное сравнение строк уязвимо к timing-атакам.

Тестирование без сервера

Используйте webhook.site для проверки входящих запросов и заголовков во время разработки.