Uygulamayı aç
Moonborn — Developers

Webhook imzasını üretim seviyesinde (production-grade) doğrula

`X-Moonborn-Signature` HMAC'ini doğrula, yeniden oynatma (replay) saldırılarını zaman damgası (timestamp) koruyucusuyla reddet, sır döndürme (secret rotation) bekleme penceresini (grace window) yönet.

Bir webhook uç noktasının üretim seviyesinde olması için üç şeyi doğru yapması gerekir: HMAC imzasını doğru hesaplamak, zaman damgasını kontrol ederek yeniden oynatma saldırısını engellemek ve sır döndürme sırasında bekleme penceresinde iki imzayı da kabul etmek. Bu rehber üçünü de Node.js + Python ile gösterir.

Eğitim seviyesinde webhook akışını Voice drift'i ele al rehberinde gördün; burada üretim seviyesinde uygulama detayı var.

Bu rehberi bitirdiğinde

  • X-Moonborn-Signature başlığının (header) biçimini (format) ayrıştırabileceksin.
  • HMAC-SHA256 imzasını zamanlama-güvenli (timing-safe) olarak karşılaştırabileceksin.
  • Yeniden oynatma saldırılarını zaman damgası penceresi kontrolüyle reddedebileceksin.
  • Sır döndürme bekleme penceresinde iki imzayı (eski + yeni) destekleyebileceksin.
  • Ham gövde (raw body) kullanmanın neden zorunlu olduğunu (yeniden serileştirme — re-serialize — hatasını) bileceksin.

Ön koşul: Bir webhook uç noktası kurulumu + imzalama sırrı (signing secret). Webhook kurulumu ve olay yükü (payload) örnekleri için Voice drift'i ele al eğitimine bak.

Başlık biçimi

Her webhook teslimi şu biçimde bir X-Moonborn-Signature başlığı taşır:

X-Moonborn-Signature: t=1747497600,v1=2c4f8a...

Alanlar:

AlanAnlamı
tİmzanın oluşturulduğu Unix zaman damgası (saniye)
v1İmzalama sırrı ile anahtarlanmış {t}.{rawBody} dizesinin hex biçimli HMAC-SHA256'sı

Tam doğrulama akışı:

  1. Başlığı ayrıştır → t ve v1'i çıkar.
  2. t çok eskiyse (örn. 5 dakikadan fazla) reddet.
  3. Beklenen imzayı HMAC-SHA256(secret, "{t}.{rawBody}") ile hesapla.
  4. Beklenen ile gelen v1'i zamanlama-güvenli karşılaştır.

Üretim seviyesinde doğrulama

import { createHmac, timingSafeEqual } from 'node:crypto';
 
const MAX_AGE_SECONDS = 300; // 5 dakika
 
export function verifyMoonbornSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('=', 2) as [string, string]),
  );
 
  const t = Number(parts['t']);
  const v1 = parts['v1'];
 
  if (!Number.isFinite(t) || typeof v1 !== 'string') return false;
 
  // Yeniden oynatma penceresi koruyucusu
  const ageSec = Math.abs(Date.now() / 1000 - t);
  if (ageSec > MAX_AGE_SECONDS) return false;
 
  // Hesapla
  const signed = `${t}.${rawBody}`;
  const expected = createHmac('sha256', secret).update(signed).digest('hex');
 
  // Zamanlama-güvenli karşılaştırma
  if (v1.length !== expected.length) return false;
  return timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
}

Neden ham gövde (raw body) kullanmak zorundasın

import express from 'express';
 
const app = express();
 
// Webhook uç noktası için JSON ayrıştırmayı değil ham gövdeyi kullan
app.post(
  '/webhooks/moonborn',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body.toString('utf8'); // Buffer → string
    const signature = req.headers['x-moonborn-signature'] as string;
 
    if (!verifyMoonbornSignature(rawBody, signature, SECRET)) {
      return res.status(401).end();
    }
 
    const event = JSON.parse(rawBody);
    // Olayı işle
    res.status(202).end();
  },
);

Yeniden oynatma penceresi — neden var, ne kadar sıkı olmalı

t yaşı reddi olmadan, geçerli bir teslimi yakalayan bir saldırgan onu sonsuza dek yeniden oynatabilir. Zaman damgası koruyucusu bu pencereyi sınırlar.

MAX_AGE_SECONDSSenaryo
300 (varsayılan, 5 dakika)Çoğu durum için makul; cömert saat kayması (clock skew) toleransı
60 (1 dakika)Bölgesel alıcı; aynı bölgede, düşük gecikme süresi
900 (15 dakika)Yüksek gecikme süresi veya çevrimdışı arabellek (offline buffering) olasılığı

Sır döndürme (secret rotation) bekleme penceresi

İmzalama sırrı döndürüldüğünde, geçiş döneminde teslimler iki v1= girdisi taşıyabilir — biri eski sırla, biri yeni sırla. Her iki adayı da kabul et:

export function verifyWithRotation(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const parts = signatureHeader.split(',');
  const t = parts.find((p) => p.startsWith('t='))?.slice(2);
  const v1Candidates = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
 
  if (!t) return false;
  const tNum = Number(t);
  if (!Number.isFinite(tNum) || Math.abs(Date.now() / 1000 - tNum) > 300) return false;
 
  const signed = `${t}.${rawBody}`;
  const expected = createHmac('sha256', secret).update(signed).digest('hex');
 
  return v1Candidates.some((v) => {
    if (v.length !== expected.length) return false;
    return timingSafeEqual(Buffer.from(v, 'hex'), Buffer.from(expected, 'hex'));
  });
}

Bekleme süresi varsayılanı: api.webhooks.secret_rotation_grace_minutes = 60. Bu pencere içinde iki imza paralel gelir; pencere dolunca sadece yeni sırla imzalanan teslimler gelir.

Doğrulamada yaygın hata kaynakları

Plan gereksinimi

Webhook'lar Team ve üstü; doğrulama mantığı her planda aynıdır.

İlgili

Voice drift'i ele al

Webhook akışını uçtan uca kurma; imza doğrulama bu rehberle birleşir.

Open →
Webhook entegrasyonu

Teslim panosu, ölü mektup kuyruğu, yeniden deneme akışı.

Open →
Webhook olay kataloğu

Tüm olay tipleri ve yük şemaları.

Open →
Kalite hattını kur

Denetim + test webhook'larını kalite kontrol kuyruğuna bağlamak.

Open →