နားလည်ထားရမယ့် အချက်
Production အတွက် webhook receiver ဆောက်တိုင်း ဖြေရှင်းရမယ့် မေးခွန်းသုံးခုကို ဒီ project က ပေါင်းစပ်ပေးပါတယ် — ဒီ request က တကယ် sender ဆီကလား၊ ဒီ event ကို တစ်ခါ handle လုပ်ပြီးသားလား၊ ပြီးတော့ ကိုယ့် processing logic မနှေးခင် response ပြန်နိုင်လားဆိုတာပါ။
Webhook security lesson ကလာတဲ့ signature verification ကတော့ HMAC သုံးပြီး payload ကို shared secret နဲ့ sign လုပ်ထားတယ်၊ ကြားထဲပြောင်းလဲမသွားဘူးလို့ သက်သေပြတာပါ — ဒါမပါရင် endpoint URL ကိုသိတဲ့သူတိုင်း provider အစစ်ဟန်ဆောင်နိုင်ပါတယ်။
Retries-and-idempotent-handlers lesson ကလာတဲ့ idempotent event processing ကတော့ webhook sender တွေက timeout သို့မဟုတ် non-2xx response ရလိုက်တိုင်း retry ပြန်လုပ်လို့ လိုအပ်တာပါ — event ID တူတိုင်း ပြန် process လုပ်နေရင် charge ဒါမှမဟုတ် notification နှစ်ခါဖြစ်သွားနိုင်ပါတယ်။
နှေးတဲ့ response က failure လို့ထင်ရတယ်
Webhook provider အများစုက timeout short ပဲပေးထားပါတယ်၊ စက္ကန့်အနည်းငယ်လောက်ပဲ — processing နှေးနေရင် endpoint fail သွားပြီလို့ ထင်ပြီး duplicate delivery ကို ပြန်ပို့တတ်ပါတယ်၊ တကယ်တော့ ဘာမှမမှားပါဘူး။
- Signature ကို အရင်ဆုံး verify လုပ်ပါ — မ verify ရသေးတဲ့ payload ကို ယုံရင် duplicate-tracking store ကို ညစ်ညမ်းစေနိုင်ပါတယ်။
- Event ID ကို ဒုတိယအနေနဲ့ စစ်ပါ — real retry တစ်ခုက အလုပ်စမလုပ်ခင် short-circuit ဖြစ်သွားသင့်ပါတယ်။
- Event က new ဖြစ်ဖြစ်၊ duplicate ဖြစ်ဖြစ်၊ invalid ဖြစ်ဖြစ် အမြဲမြန်မြန် response ပြန်ပေးပါ။
WEBHOOK RECEIVER FLOW
---------------------
Sender (e.g. Stripe, GitHub)
|
| POST /webhook (payload + X-Signature-256 header)
v
1. Verify Signature (HMAC-SHA256)
-- invalid --> 401 Unauthorized (stop, do not process)
-- valid --> continue
|
v
2. Check Event ID against processed-events store
-- new --> process the event, then record its ID
-- duplicate --> skip (do not reprocess), no side effects
|
v
3. Respond 200 OK
(always -- new, duplicate, or invalid all get a fast
response -- well before the sender's own timeout expires)လက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်
Signing helper ရေးပါ
Raw request body ကို shared secret နဲ့ HMAC-SHA256 digest ဖြစ်အောင် တွက်ပါ — sender ရဲ့ signature header ကို ဖန်တီးခဲ့တဲ့ algorithm အတိအကျ ဖြစ်ရပါမယ်။
timingSafeEqual နဲ့ verify လုပ်ပါ
Plain string comparison အစား crypto.timingSafeEqual သုံးပါ — ပုံမှန် equality check က timing attack ကို ဖြစ်စေနိုင်ပါတယ်။
Duplicate check store ဆောက်ပါ
Sender ပေးထားတဲ့ event ID နဲ့ key ချပြီး processed ပြီးသားလား မှတ်ထားတဲ့ store တစ်ခု ဆောက်ပါ (Set, database, ဒါမှမဟုတ် Redis)။
handleWebhook ထဲ ဆက်စပ်ပါ
Verify -> check -> process-or-skip -> အမြဲ response ချက်ချင်း ပြန်ပေးတဲ့ function တစ်ခုတည်းအဖြစ် ပေါင်းစပ်ပါ။
Scenario သုံးခု စမ်းသပ်ပါ
Event အသစ်၊ event ထပ်တူ (duplicate)၊ signature မှား — သုံးခုလုံး မှန်ကန်စွာ အလုပ်လုပ်ကြောင်း သေချာအောင် စစ်ပါ။
Real server ချိတ်ခင် test လုပ်ပါ
handleWebhook function ကို scenario သုံးခုလုံးနဲ့ စမ်းပြီးမှ Express ဒါမှမဟုတ် Node's http module နောက်ကွယ်မှာ ချိတ်ဆက်ပါ။
အတူတူ စမ်းရေးကြည့်မယ်
const crypto = require('crypto');
// In-memory "database" of event IDs we have already processed.
// In a real service this would be a row in Postgres/Redis, not a Set.
const processedEvents = new Set();
function signPayload(payload, secret) {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
function handleWebhook(payload, signatureHeader, secret) {
// Step 1: verify the signature BEFORE touching any event data.
const expected = signPayload(payload, secret);
const provided = signatureHeader.replace('sha256=', '');
const validLength = provided.length === expected.length;
const isValid =
validLength &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
if (!isValid) {
return { status: 401, body: { ok: false, reason: 'invalid_signature' } };
}
// Step 2: only after the signature is trusted, look at the event ID.
const event = JSON.parse(payload);
if (processedEvents.has(event.id)) {
return {
status: 200,
body: { ok: true, reason: 'duplicate_skipped', eventId: event.id },
};
}
// Step 3: process the new event, then record it as done.
processedEvents.add(event.id);
return {
status: 200,
body: { ok: true, reason: 'processed', eventId: event.id, type: event.type },
};
}
// --- Fixed, deterministic test data -----------------------------------
const secret = 'whsec_test_secret';
const payload = JSON.stringify({ id: 'evt_1001', type: 'payment.succeeded', amount: 4200 });
const goodSig = 'sha256=' + signPayload(payload, secret);
const badSig = 'sha256=' + '0'.repeat(64);
console.log('Scenario 1: valid new event');
console.log(handleWebhook(payload, goodSig, secret));
console.log('\nScenario 2: valid duplicate event (same payload again)');
console.log(handleWebhook(payload, goodSig, secret));
console.log('\nScenario 3: invalid signature');
console.log(handleWebhook(payload, badSig, secret));Scenario 1 (event အသစ်) က { status: 200, body: { ok: true, reason: 'processed', eventId: 'evt_1001', type: 'payment.succeeded' } } ပြန်ပေးပါတယ်။ Scenario 2 (event ထပ်တူ) က { status: 200, body: { ok: true, reason: 'duplicate_skipped', eventId: 'evt_1001' } } ပြန်ပေးပြီး ပြန် process မလုပ်ပါဘူး။ Scenario 3 (signature မှား) က event ကို parse လုပ်ခြင်းမပြုမီ { status: 401, body: { ok: false, reason: 'invalid_signature' } } နဲ့ reject ဖြစ်ပါတယ်။၅ မိနစ် စမ်းကြည့်
handleWebhook ကို processedEvents ထဲမှာ timestamp ပါ သိမ်းဆည်းအောင် တိုးချဲ့ပါ၊ ပြီးရင် နောက်ဆုံး ၂၄ နာရီအတွင်းက entry တွေကိုပဲ ထားရှိတဲ့ cleanup step တစ်ခု ထည့်ပါ (real time အစား simulated clock ကို သုံးပါ) — ဒါက production system တစ်ခုက duplicate-tracking store ကို အကန့်အသတ်မဲ့ ကြီးလာခြင်းမှ ကာကွယ်ပုံကို ထင်ဟပ်ပါတယ်။
သတိလေးတစ်ချက်
Signature ကို verify မလုပ်ခင် event ကို process လုပ်မိတာ — "log ချရုံပဲ" ဆိုပြီးလုပ်ရင်တောင် forge လုပ်ထားတဲ့ payload ကို legitimate ဖြစ်ဟန် မှတ်တမ်းတင်ပေးလိုက်တာနဲ့ တူပါတယ်။
Signature ကို plain === သို့မဟုတ် string comparison နဲ့ စစ်တာ — crypto.timingSafeEqual မသုံးရင် timing information ပေါက်ကြားပြီး attacker က signature ကို ခန့်မှန်းနိုင်ပါတယ်။
Stripe Docs: Verify webhook signatures — API Integration & Webhooks