Panduan

Panduan11 menit baca

Verifikasi tanda tangan webhook formulir

Pastikan webhook formulir benar-benar berasal dari Ikiform: skema HMAC-SHA256 Standard Webhooks, test vector, body mentah, timestamp, dan rotasi secret.

Preet SutharPendiri, Ikiform

Hitung ulang tanda tangan sendiri dan bandingkan dengan yang ada di request. Ikiform mengikuti Standard Webhooks: tanda tangannya adalah HMAC-SHA256 base64 dari webhook-id, webhook-timestamp, dan body mentah yang digabung dengan titik, dengan key berupa bagian hasil decode base64 dari secret whsec_ Anda, dan dikirim sebagai v1,<signature>. Terima request hanya jika satu tanda tangan v1 cocok dalam perbandingan constant-time dan timestamp berada dalam 5 menit.

Endpoint webhook adalah URL publik yang menerima request POST dari siapa pun. Tanpa pemeriksaan tanda tangan, siapa pun yang tahu URL-nya bisa mengirim kiriman palsu ke CRM, database, atau chat tim Anda. Pemeriksaannya hanya beberapa baris kode, dan menulis baris-baris itu dengan tepat adalah inti panduan ini.

Cara kerja penandatanganan

Ikiform dan server Anda berbagi satu secret, yang ditampilkan sekali saat Anda menambahkan endpoint. Sebelum setiap percobaan pengiriman, Ikiform meng-hash request dengan secret itu dan menaruh hasilnya di header. Server Anda mengulang perhitungan itu pada apa yang diterimanya. Hanya pihak yang memegang secret yang bisa menghasilkan nilai yang cocok, dan perubahan apa pun pada body, id, atau timestamp akan merusaknya.

Bagaimana Ikiform menandatangani pengiriman webhook dan bagaimana server Anda memeriksanyaIkiform, di setiap percobaanhandler Andabody JSONsubmission.createdid.timestamp.bodykonten yangditandatanganiHMAC-SHA256dengan setiapsecret aktifwebhook-signaturev1,<base64>body mentah+ headersebelumparsing apa punHMAC yangsamadengan salinansecret Andaperbandinganconstant-timeada v1 yangcocok,timestamp baru2xxlalu parse dantangani400tidak ada yangditanganitidak cocokBagaimana Ikiform menandatangani pengiriman webhook dan bagaimana server Anda memeriksanyaIkiform, di setiap percobaanhandler Andabody JSONsubmission.createdid.timestamp.bodykonten yangditandatanganiHMAC-SHA256dengan setiapsecret aktifwebhook-signaturev1,<base64>body mentah +headersebelum parsing apapunHMAC yang samadengan salinansecret Andaperbandinganconstant-timeada v1 yang cocok,timestamp baru2xxlalu parse dantangani400tidak adayangditanganitidak cocok
  1. Ikiform menserialisasi event sebagai JSON.
  2. Ikiform menggabungkan id pengiriman, Unix timestamp percobaan, dan body dengan titik.
  3. Ikiform menghitung HMAC-SHA256 dari string itu dengan setiap secret aktif milik endpoint.
  4. Ikiform mengirim setiap hasil, di-encode base64 dan diberi awalan v1, di header webhook-signature.
  5. Handler Anda membaca body mentah dan ketiga header sebelum mem-parsing apa pun.
  6. Handler menghitung HMAC yang sama dengan salinan secret-nya.
  7. Handler membandingkan secara constant-time dengan setiap tanda tangan v1 dan memeriksa timestamp berada dalam 5 menit.
  8. Jika cocok, handler menjawab 2xx dan menangani event. Jika tidak, handler menjawab 400 dan tidak melakukan apa pun.
Tidak ada yang rahasia ikut terkirim bersama request. Tanda tangan membuktikan bahwa pengirim tahu secret-nya, dan bahwa byte-nya tidak diubah di perjalanan.

Skemanya, secara persis

Ikiform memakai Standard Webhooks, spesifikasi terbuka yang dipakai bersama oleh beberapa penyedia webhook. Setiap pengiriman membawa tiga header:

HeaderNilaiCatatan
webhook-iddlv_...Id pengiriman. Sama di setiap percobaan ulang, jadi sekaligus berfungsi sebagai idempotency key
webhook-timestampDetik Unix, seperti 1792300000Kapan percobaan ini ditandatangani. Baru di setiap percobaan ulang
webhook-signaturev1, ditambah base64Satu atau lebih nilai yang dipisahkan spasi. Dua saat secret sedang dirotasi
Apa yang ditandatangani
webhook-id:        dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
raw body:          {"id":"dlv_01k7...","type":"endpoint.test",...}

signed content = webhook-id + "." + webhook-timestamp + "." + raw body
key            = base64-decode(secret without "whsec_")
signature      = base64(HMAC-SHA256(key, signed content))
header         = "v1," + signature

Untuk memverifikasi pengiriman, dengan urutan ini:

  1. Baca body mentah sebagai byte atau string persis. Jangan di-parse dulu.
  2. Baca ketiga header. Jika ada yang hilang, tolak.
  3. Pastikan timestamp berupa bilangan bulat dalam 5 menit dari jam Anda, ke arah mana pun. Jika tidak, tolak.
  4. Hapus whsec_ dari secret Anda dan decode base64 sisanya. 32 byte itulah key HMAC-nya, bukan string itu sendiri.
  5. Hitung HMAC-SHA256 dari {webhook-id}.{webhook-timestamp}.{raw body} dan encode base64 dengan alfabet standar dan padding.
  6. Pisahkan webhook-signature berdasarkan spasi. Untuk setiap entri, pisahkan di koma pertama; lewati apa pun yang versinya bukan v1.
  7. Bandingkan setiap nilai v1 dengan milik Anda secara constant-time. Jika ada yang cocok, pengiriman itu asli.
  8. Baru sekarang parse JSON dan tindak lanjuti.

Test vector

Sebelum men-deploy handler, buktikan bahwa handler menghitung nilai yang benar. Input ini adalah body pengiriman endpoint.test sungguhan yang ditandatangani dengan secret karangan. Kode Anda harus menghasilkan persis tanda tangan di baris terakhir. Setiap panduan bahasa menyertakan unit test yang memakainya.

InputNilai
Secretwhsec_Fg8c6hm1Gt41v5RSPKm72LggDx8S1mlWkbBYpHPAnpw=
webhook-iddlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp1792300000
Body mentahByte persis di script di bawah, tanpa spasi atau baris baru
webhook-signature yang diharapkanv1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA=

Anda bisa memeriksanya tanpa menulis kode. Ini berjalan di terminal macOS atau Linux dan mencetak bagian base64 dari tanda tangan:

Terminal
SECRET='whsec_Fg8c6hm1Gt41v5RSPKm72LggDx8S1mlWkbBYpHPAnpw='
ID='dlv_01k7v2d4n8r2p6s0w3y5b7c9e1'
TS='1792300000'
BODY='{"id":"dlv_01k7v2d4n8r2p6s0w3y5b7c9e1","type":"endpoint.test","timestamp":"2026-10-17T12:00:00.000Z","api_version":"2026-10-17","data":{"endpoint_id":"whk_01k7v2e1b3d5f7h9k1m3n5q7s9","message":"This is a test event from Ikiform."}}'

KEY=$(printf '%s' "${SECRET#whsec_}" | base64 -d | xxd -p -c 256)
printf '%s.%s.%s' "$ID" "$TS" "$BODY" \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary \
  | base64

Dua kasus lagi layak diuji masing-masing, karena keduanya adalah kegagalan nyata yang paling umum:

PerubahanTanda tanganHaruskah handler Anda menerimanya?
Body yang sama diformat rapi dengan indentasi dua spasiv1,se0TDhknrG0hYMvCId83HnpZfJcELG3ozxFJktRUHqY=Tidak. JSON sama, byte berbeda
webhook-timestamp satu detik kemudianv1,oBfWyLDPfrwVSo40e6b7B7+LmKkZX7OqT88SsrpY6ks=Tidak. Timestamp ikut ditandatangani
Ditandatangani dengan secret kedua saat rotasiv1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=Ya, jika header juga punya nilai yang cocok dengan secret Anda

Saat rotasi secret, header membawa kedua nilai. Handler Anda, yang memegang salah satu secret, harus menerima ini:

Dua tanda tangan
webhook-id: dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
webhook-signature: v1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA= v1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=
Timestamp di vector bersifat tetap, jadi verifier sungguhan menolaknya karena terlalu lama. Test harus memberikan waktu saat ini yang tetap, seperti yang dilakukan panduan bahasa. Signature verifier di halaman webhook memeriksa secret, header, dan body apa pun di browser.

Cara memverifikasi

PendekatanUnggul dalamKekurangan
verifyWebhook dari @ikiform/sdk/webhooksSatu panggilan yang memeriksa semuanya dan mengembalikan event bertipe. Berjalan di Node, Workers, Deno, dan BunHanya JavaScript dan TypeScript
Library Standard WebhooksKode terawat untuk Python, Go, PHP, Ruby, JavaScript, Rust, Java, C#, dan ElixirSatu dependency lagi. Periksa cara masing-masing menerima header; sebagian case-sensitive
Kode HMAC Anda sendiriTanpa dependency, sekitar 20 baris, dan Anda bisa menguji setiap cabangMudah keliru secara halus: perbandingan string, byte key yang salah, atau body yang sudah di-parse
Path rahasia atau token query sebagai gantinyaSederhanaBocor lewat log, proxy, dan error tracker, tidak membuktikan apa pun tentang body, dan tidak bisa kedaluwarsa. Bukan pengganti

Ikiform menandatangani setiap pengiriman, dan tanda tangan adalah cara yang didukung untuk mengautentikasinya. Dokumentasinya tidak memublikasikan daftar tetap alamat IP pengirim, jadi allowlist IP bukan pemeriksaan yang bisa Anda andalkan.

Verifikasi di framework Anda

Untuk stack JavaScript, SDK yang melakukan pekerjaannya. Yang berbeda antar framework adalah di mana route berada dan cara mendapatkan body sebelum ada yang mem-parsing-nya. Pilih milik Anda:

Route handler App Router. request.text() adalah body mentah; memanggil request.json() lebih dulu akan menghilangkannya. Panduan server sendiri menambahkan dedupe dan after().

app/api/webhooks/ikiform/route.ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    console.info(event.type, request.headers.get("webhook-id"));
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}

Setiap handler menjawab 204 saat sukses dan 400 beserta alasan kegagalan jika tidak. Lakukan pekerjaan lambat setelah menjawab; Ikiform menunggu 15 detik sebelum menganggap percobaan gagal.

Mengapa body mentah penting

Tanda tangan mencakup byte, bukan makna. {"a":1} dan { "a": 1 } adalah JSON yang sama dengan byte berbeda, jadi body yang sudah di-parse lalu diserialisasi ulang hampir tidak pernah cocok. Hal yang sama berlaku untuk apa pun yang menyentuh body saat masuk: middleware yang mem-parsing JSON, proxy yang meng-encode ulang, atau framework yang memberi Anda objek alih-alih string.

Baca body sekali, sebagai byte atau string, verifikasi, lalu parse string yang sama itu. Setiap panduan bahasa menunjukkan di mana body mentah berada di framework-nya, dan panduan server sendiri punya tabel untuk framework Node dan Python yang umum.

Bandingkan secara constant-time

Perbandingan string biasa berhenti di karakter pertama yang berbeda. Secara teori, penyerang yang bisa mengukur waktu ribuan request akan tahu berapa banyak karakter awal tebakannya yang benar dan menyusun tanda tangan valid satu karakter demi satu karakter. Perbandingan constant-time memakan waktu yang sama berapa pun bagian yang cocok. Setiap bahasa punya:

BahasaPerbandingan constant-time
Node.jscrypto.timingSafeEqual(a, b), yang melempar error jika panjangnya berbeda, jadi periksa panjang lebih dulu
Pythonhmac.compare_digest(a, b)
Gohmac.Equal(a, b)
PHPhash_equals($known, $user)
RubyOpenSSL.secure_compare(a, b), atau ActiveSupport::SecurityUtils.secure_compare di Rails

Timestamp dan replay

Request valid yang tertangkap sekali akan tetap valid selamanya kecuali ada yang membuatnya kedaluwarsa. Timestamp yang ditandatangani itulah yang melakukannya: tolak pengiriman apa pun yang selisihnya lebih dari 5 menit dari jam Anda, ke arah mana pun. Karena Ikiform menandatangani ulang setiap percobaan dengan timestamp baru, percobaan ulang tiga hari kemudian tetap lolos; hanya request lama yang disalin yang gagal.

Dalam 5 menit itu, replay masih mungkin, begitu pula duplikat yang asli: Ikiform mencoba ulang saat tidak mendapat 2xx tepat waktu, meski handler Anda sudah selesai. Simpan webhook-id dengan unique constraint dan lewati id yang sudah Anda tangani. Itu menutup keduanya.

Jaga jam server tetap sinkron. Jam yang bergeser lebih dari 5 menit menolak setiap pengiriman dengan timestamp_too_old atau timestamp_too_new. Platform terkelola biasanya menjaga jam tetap sinkron; di VM Anda sendiri, jalankan NTP.

Merotasi secret

Rotasi secret saat orang yang mengetahuinya keluar, saat secret mungkin tercatat di log atau ter-commit, atau secara terjadwal. Roll signing secret di halaman endpoint membuat secret baru, dan selama 24 jam setiap pengiriman ditandatangani dengan keduanya:

Isi webhook-signature sebelum, selama, dan setelah rotasi secretsebelumv1,<old>rotasisecret baruditampilkansekali24 jamberikutnyav1,<old>v1,<new>sesudahhanyav1,<new>deploy yang baruIsi webhook-signature sebelum, selama, dan setelah rotasi secretsebelumv1,<old>rotasisecret baru ditampilkan sekali24 jam berikutnyav1,<old> v1,<new>sesudahhanya v1,<new>deploy yang baru
  1. Sebelum rotasi, setiap pengiriman membawa satu tanda tangan, dibuat dengan secret lama.
  2. Anda memilih Roll signing secret dan menyalin secret baru.
  3. Selama 24 jam berikutnya, setiap pengiriman membawa dua tanda tangan, jadi server yang memegang salah satu secret akan menerimanya. Deploy secret baru dalam jendela waktu ini.
  4. Setelah 24 jam, pengiriman hanya membawa tanda tangan baru, dan server yang masih memegang secret lama menolaknya.
Handler Anda tidak butuh kode khusus untuk ini, selama handler memeriksa setiap entri v1, bukan hanya yang pertama.

Jika secret lama masih ter-deploy setelah 24 jam, setiap pengiriman gagal. Ikiform mencoba ulang masing-masing 8 kali selama sekitar tiga hari, jadi setelah Anda men-deploy secret yang benar, gunakan Retry di log pengiriman untuk yang sudah menyerah.

Setelah pemeriksaan lolos

Tanda tangan yang valid membuktikan request berasal dari Ikiform dan tidak diubah. Itu tidak membuktikan kontennya aman atau baru. Tiga kebiasaan menyelesaikan pekerjaan ini:

  • Lewati pengulangan. Simpan webhook-id, atau id di payload, yang nilainya sama, dengan unique constraint. Percobaan ulang dari pengiriman yang sudah Anda tangani menjadi no-op.
  • Jawab dulu, kerjakan belakangan. Kembalikan 2xx begitu event disimpan atau dimasukkan ke antrean. Ikiform menganggap apa pun yang lebih lambat dari 15 detik sebagai kegagalan dan mencoba ulang, yang memunculkan kembali masalah duplikat.
  • Perlakukan field sebagai tidak tepercaya. data.fields berisi apa yang diketik pengunjung di formulir publik. Escape sebelum merendernya di HTML, pesan chat, atau email, dan jangan pernah meneruskannya ke shell atau query tanpa parameter.

Menguji sebelum live

Ikiform hanya mengirim ke URL https: publik di port 443, jadi localhost tidak bisa menerima webhook. Jalankan tunnel seperti cloudflared tunnel --url http://localhost:3000 dan tambahkan URL yang dicetaknya sebagai endpoint kedua selama Anda bekerja. Send test di halaman endpoint mengirim event endpoint.test sekali, tanpa percobaan ulang, ditandatangani persis seperti pengiriman sungguhan. Hapus endpoint uji setelah selesai; endpoint itu punya secret sendiri, jadi tidak ada yang lain berubah.

Jika pengiriman gagal dan Anda tidak tahu sebabnya, salin header dan body-nya dari log Anda ke signature verifier. Alat itu menjalankan pemeriksaan yang sama di browser dan memberi tahu langkah mana yang gagal.

Lima cara verifikasi bisa salah

KesalahanGejalaSolusi
Memakai seluruh string whsec_... sebagai keyno_matching_signature di setiap pengirimanHapus whsec_, lalu decode base64. Key-nya 32 byte
Memverifikasi body yang sudah di-parseno_matching_signature, terkadang hanya untuk sebagian payloadVerifikasi body mentah; parse sesudahnya
Hanya memeriksa tanda tangan pertamaKegagalan yang dimulai tepat setelah rotasiLoop setiap entri v1 yang dipisahkan spasi
Heksadesimal alih-alih base64no_matching_signatureStandard Webhooks memakai base64 standar dengan padding, bukan hex atau base64url
Mencari header dengan huruf besar-kecil yang persismissing_headers di balik beberapa proxyNama header tidak peka huruf besar-kecil. Ikiform mengirimnya dalam huruf kecil; cari tanpa memedulikan huruf besar-kecil

Tidak ada integrasi sekali klik. Setiap alat dijangkau lewat webhook bertanda tangan, langsung atau melalui alat otomasi seperti Zapier atau n8n. Satu workspace bisa punya 3 endpoint, masing-masing dengan secret sendiri. Payload lengkap, jadwal percobaan ulang, dan log pengiriman ada di Webhooks.

Pilih situasi Anda

Setiap halaman bahasa berisi handler yang idiomatis, opsi library, dan unit test yang menjalankan test vector dari halaman ini.

Pastikan berfungsi

Lakukan ini di endpoint staging dulu, lalu sekali lagi di produksi. Ikiform tidak punya paket gratis. Pengujian gratis: workspace tanpa paket menerima 50 kiriman uji per bulan dari localhost. Untuk live, pilih Pro mulai $6/bln untuk 500 kiriman, atau bayar sekali: 10.000 kiriman seharga $49 atau 50.000 kiriman seharga $149, dengan kredit yang tidak pernah kedaluwarsa. Lihat harga.

  1. Jalankan test vector lewat kode Anda: kode harus menghasilkan tanda tangan yang diharapkan, menerima header dengan dua tanda tangan, dan menolak body yang diformat rapi.
  2. Di dashboard, buka Webhooks, tambahkan URL https: publik endpoint Anda, dan salin secret whsec_ ke environment server Anda.
  3. Pilih Send test di halaman endpoint. Log pengiriman menampilkan 204 Anda dan event sampai ke log Anda.
  4. Ubah satu karakter secret di server Anda dan kirim uji lagi. Log menampilkan 400 dengan no_matching_signature.
  5. Kirim formulir sungguhan dan pastikan pengiriman submission.created berhasil.

Pemecahan masalah

MasalahYang harus dilakukan
400 no_matching_signatureSecret salah, body sudah di-parse, atau key yang tidak di-decode base64. Jalankan test vector lewat jalur kode yang sama untuk melihat yang mana.
400 timestamp_too_old atau timestamp_too_newJam server meleset, atau Anda memutar ulang request tersimpan secara manual. Sinkronkan jam, dan gunakan Retry di log pengiriman, yang menandatangani ulang.
400 missing_headersProxy atau framework membuang header webhook-*, atau request tidak berasal dari Ikiform. Catat setiap nama header yang dilihat handler Anda.
Berhasil untuk event uji, gagal untuk kiriman sungguhanPayload sungguhan berisi karakter non-ASCII dan teks yang lebih panjang. Kode Anda men-decode atau meng-encode ulang body sebelum verifikasi. Verifikasi byte yang persis.
Setiap pengiriman gagal tepat setelah merotasi secretHandler hanya memeriksa tanda tangan pertama, atau 24 jam sudah lewat dengan secret lama masih ter-deploy. Periksa setiap entri v1 dan deploy secret baru.
Log pengiriman menampilkan timeout, padahal handler Anda berjalanIkiform menunggu 15 detik. Jawab dulu dan lakukan pekerjaan lambat sesudahnya, dan lewati nilai webhook-id yang berulang.

Pertanyaan

Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.