webhook-security
Build webhook signature verification for Paymob, Stripe, Resend, and generic HMAC. Triggers when user asks to verify webhook signatures, add HMAC validation, handle payment webhooks (Paymob/Stripe), i
SKILL.mdskills/webhook-security/SKILL.md
GitHubWebhook Security Agent
Step 1 — Identify the Platform
- Laravel (PHP) → middleware + controller approach
- Next.js (Node) → API route approach
- Go → http.Handler approach
- Generic → all approaches with stack-specific sections
Step 2 — Choose the Gateway
Paymob (Egypt)
- HMAC-SHA512 signature in
hmacquery param - Raw body must be verified:
hash(hmac_secret, raw_body) - Verify: transaction
successfield + merchant order ID - Refund: POST /v1/cashout/transactions/{id}/refund
- Idempotency key: merchant_order_id
Stripe
- HMAC-SHA256 with
Stripe-Signatureheader,t=timestamp +v1=signature - Verify:
timing_safe_equalto prevent timing attacks - Replay protection: check timestamp within 300s window
- Idempotency key: Stripe-Idempotency-Key header
Resend
- HMAC-SHA256 with
Resend-Signatureheader - Verify: raw body,
Resend-Webhook-Secretenv var - Replay protection: same timestamp window as Stripe
Generic HMAC
- Any string secret, any hash algorithm
- Verify: constant-time comparison
- Replay protection: optional timestamp validation
Step 3 — Implement in Laravel
// routes/web.php
Route::post('/webhook/paymob', [WebhookController::class, 'paymob']);
Route::post('/webhook/stripe', [WebhookController::class, 'stripe']);
Route::post('/webhook/resend', [WebhookController::class, 'resend']);
// app/Http/Middleware/VerifyWebhookSignature.php
class VerifyWebhookSignature {
public function handle($request, $next, $secret, $algo = 'sha512') {
$signature = $request->input('hmac') ?? $request->header('Stripe-Signature');
$payload = $request->getContent();
$expected = hash_hmac($algo, $payload, $secret);
if (!hash_equals($expected, $signature)) {
abort(401, 'Invalid signature');
}
return $next($request);
}
}
Step 4 — Implement in Next.js
// app/api/webhook/paymob/route.ts
export async function POST(req: Request) {
const raw = await req.text();
const sig = req.headers.get('paymob-hmac') ?? '';
const expected = crypto.createHmac('sha512', process.env.PAYMOB_HMAC_SECRET!)
.update(raw).digest('hex');
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return Response.json({ error: 'Invalid signature' }, { status: 401 });
}
const body = JSON.parse(raw);
// process...
return Response.json({ received: true });
}
Step 5 — Idempotency + Replay Prevention
- Store processed
event_idin Redis/DB with 24h TTL - Skip if already processed
- Use merchant_order_id as idempotency key for Paymob
- Use Stripe-Idempotency-Key for Stripe
Step 6 — Error Handling
- Return 200 only after processing is complete
- Queue failures → return 202 Accepted (processing async)
- Return 500 only for critical failures (trigger retry from provider)
- Log all webhook events to a webhook_events table
Step 7 — Testing
- Use ngrok/Cloudflare Tunnel for local webhook testing
- Stripe:
stripe listen --forward-to localhost:3000/api/webhook/stripe - Paymob: set test mode in dashboard, use Paymob test cards