296 lines
7.6 KiB
Markdown
296 lines
7.6 KiB
Markdown
# Payment Webhook Configuration
|
|
|
|
Generate secure webhook handlers for payment providers.
|
|
|
|
## Task
|
|
|
|
You are a payment webhook security expert. Generate secure, production-ready webhook handlers.
|
|
|
|
### Steps:
|
|
|
|
1. **Ask for Provider**:
|
|
- Stripe
|
|
- PayPal
|
|
- Square
|
|
- Custom payment gateway
|
|
|
|
2. **Generate Webhook Endpoint** (Stripe):
|
|
|
|
```typescript
|
|
import crypto from 'crypto';
|
|
import express from 'express';
|
|
import Stripe from 'stripe';
|
|
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
const app = express();
|
|
|
|
// CRITICAL: Use raw body for webhook signature verification
|
|
app.post(
|
|
'/api/webhooks/stripe',
|
|
express.raw({ type: 'application/json' }),
|
|
async (req, res) => {
|
|
const sig = req.headers['stripe-signature'];
|
|
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
|
|
|
let event;
|
|
|
|
try {
|
|
// Verify webhook signature
|
|
event = stripe.webhooks.constructEvent(
|
|
req.body,
|
|
sig!,
|
|
webhookSecret
|
|
);
|
|
} catch (err) {
|
|
console.error(`Webhook signature verification failed: ${err.message}`);
|
|
return res.status(400).send(`Webhook Error: ${err.message}`);
|
|
}
|
|
|
|
// Handle event idempotently
|
|
const eventId = event.id;
|
|
const existingEvent = await db.webhookEvents.findUnique({
|
|
where: { stripeEventId: eventId },
|
|
});
|
|
|
|
if (existingEvent) {
|
|
console.log(`Duplicate webhook event: ${eventId}`);
|
|
return res.status(200).json({ received: true });
|
|
}
|
|
|
|
// Store event (prevents duplicate processing)
|
|
await db.webhookEvents.create({
|
|
data: {
|
|
stripeEventId: eventId,
|
|
type: event.type,
|
|
processedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
// Process event in background to return 200 quickly
|
|
processWebhookEvent(event).catch((error) => {
|
|
console.error(`Failed to process webhook: ${error.message}`);
|
|
// Alert ops team
|
|
});
|
|
|
|
res.status(200).json({ received: true });
|
|
}
|
|
);
|
|
|
|
async function processWebhookEvent(event: Stripe.Event) {
|
|
switch (event.type) {
|
|
case 'checkout.session.completed':
|
|
await handleCheckoutComplete(event.data.object);
|
|
break;
|
|
|
|
case 'customer.subscription.created':
|
|
case 'customer.subscription.updated':
|
|
await handleSubscriptionChange(event.data.object);
|
|
break;
|
|
|
|
case 'customer.subscription.deleted':
|
|
await handleSubscriptionCanceled(event.data.object);
|
|
break;
|
|
|
|
case 'invoice.paid':
|
|
await handleInvoicePaid(event.data.object);
|
|
break;
|
|
|
|
case 'invoice.payment_failed':
|
|
await handleInvoicePaymentFailed(event.data.object);
|
|
break;
|
|
|
|
case 'payment_intent.succeeded':
|
|
await handlePaymentSuccess(event.data.object);
|
|
break;
|
|
|
|
case 'payment_intent.payment_failed':
|
|
await handlePaymentFailed(event.data.object);
|
|
break;
|
|
|
|
case 'charge.dispute.created':
|
|
await handleDisputeCreated(event.data.object);
|
|
break;
|
|
|
|
case 'customer.created':
|
|
await handleCustomerCreated(event.data.object);
|
|
break;
|
|
|
|
default:
|
|
console.log(`Unhandled event type: ${event.type}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Generate PayPal Webhook**:
|
|
|
|
```typescript
|
|
import crypto from 'crypto';
|
|
|
|
app.post('/api/webhooks/paypal', express.json(), async (req, res) => {
|
|
const webhookId = process.env.PAYPAL_WEBHOOK_ID!;
|
|
const webhookEvent = req.body;
|
|
|
|
// Verify PayPal webhook signature
|
|
const isValid = await verifyPayPalWebhook(req, webhookId);
|
|
|
|
if (!isValid) {
|
|
return res.status(400).send('Invalid webhook signature');
|
|
}
|
|
|
|
const eventType = webhookEvent.event_type;
|
|
|
|
switch (eventType) {
|
|
case 'PAYMENT.CAPTURE.COMPLETED':
|
|
await handlePayPalPaymentCompleted(webhookEvent.resource);
|
|
break;
|
|
|
|
case 'BILLING.SUBSCRIPTION.CREATED':
|
|
await handlePayPalSubscriptionCreated(webhookEvent.resource);
|
|
break;
|
|
|
|
case 'BILLING.SUBSCRIPTION.CANCELLED':
|
|
await handlePayPalSubscriptionCancelled(webhookEvent.resource);
|
|
break;
|
|
|
|
default:
|
|
console.log(`Unhandled PayPal event: ${eventType}`);
|
|
}
|
|
|
|
res.status(200).json({ received: true });
|
|
});
|
|
|
|
async function verifyPayPalWebhook(req, webhookId) {
|
|
const transmissionId = req.headers['paypal-transmission-id'];
|
|
const timestamp = req.headers['paypal-transmission-time'];
|
|
const signature = req.headers['paypal-transmission-sig'];
|
|
const certUrl = req.headers['paypal-cert-url'];
|
|
|
|
const response = await fetch(
|
|
`https://api.paypal.com/v1/notifications/verify-webhook-signature`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${await getPayPalAccessToken()}`,
|
|
},
|
|
body: JSON.stringify({
|
|
transmission_id: transmissionId,
|
|
transmission_time: timestamp,
|
|
cert_url: certUrl,
|
|
auth_algo: req.headers['paypal-auth-algo'],
|
|
transmission_sig: signature,
|
|
webhook_id: webhookId,
|
|
webhook_event: req.body,
|
|
}),
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
return data.verification_status === 'SUCCESS';
|
|
}
|
|
```
|
|
|
|
4. **Generate Webhook Testing Script**:
|
|
|
|
```typescript
|
|
// test-webhook.ts
|
|
import { exec } from 'child_process';
|
|
import util from 'util';
|
|
|
|
const execPromise = util.promisify(exec);
|
|
|
|
async function testWebhook() {
|
|
// Install Stripe CLI: brew install stripe/stripe-cli/stripe
|
|
|
|
// Listen to webhooks
|
|
const { stdout } = await execPromise('stripe listen --forward-to localhost:3000/api/webhooks/stripe');
|
|
console.log(stdout);
|
|
|
|
// Trigger test events
|
|
await execPromise('stripe trigger payment_intent.succeeded');
|
|
await execPromise('stripe trigger customer.subscription.created');
|
|
await execPromise('stripe trigger invoice.payment_failed');
|
|
}
|
|
|
|
testWebhook();
|
|
```
|
|
|
|
5. **Generate Monitoring & Alerting**:
|
|
|
|
```typescript
|
|
// Monitor webhook failures
|
|
async function monitorWebhooks() {
|
|
const failedEvents = await db.webhookEvents.findMany({
|
|
where: {
|
|
processed: false,
|
|
createdAt: {
|
|
lt: new Date(Date.now() - 5 * 60 * 1000), // 5 minutes ago
|
|
},
|
|
},
|
|
});
|
|
|
|
if (failedEvents.length > 0) {
|
|
await sendAlert({
|
|
type: 'webhook_failure',
|
|
count: failedEvents.length,
|
|
events: failedEvents.map((e) => e.type),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Retry failed webhooks
|
|
async function retryFailedWebhooks() {
|
|
const failedEvents = await db.webhookEvents.findMany({
|
|
where: { processed: false },
|
|
take: 10,
|
|
});
|
|
|
|
for (const event of failedEvents) {
|
|
try {
|
|
await processWebhookEvent(event.data);
|
|
await db.webhookEvents.update({
|
|
where: { id: event.id },
|
|
data: { processed: true, processedAt: new Date() },
|
|
});
|
|
} catch (error) {
|
|
console.error(`Retry failed for event ${event.id}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
6. **Generate Webhook Schema** (Database):
|
|
|
|
```sql
|
|
CREATE TABLE webhook_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
stripe_event_id VARCHAR(255) UNIQUE NOT NULL,
|
|
type VARCHAR(100) NOT NULL,
|
|
data JSONB NOT NULL,
|
|
processed BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
processed_at TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_webhook_events_type ON webhook_events(type);
|
|
CREATE INDEX idx_webhook_events_processed ON webhook_events(processed);
|
|
```
|
|
|
|
### Security Best Practices:
|
|
|
|
- ✅ Verify webhook signatures (prevent spoofing)
|
|
- ✅ Use raw request body for signature validation
|
|
- ✅ Idempotency (track event IDs, prevent duplicate processing)
|
|
- ✅ Return 200 immediately (process in background)
|
|
- ✅ Retry logic for failures
|
|
- ✅ Monitoring and alerting
|
|
- ✅ HTTPS only (secure transmission)
|
|
- ✅ IP whitelisting (optional but recommended)
|
|
|
|
### Example Usage:
|
|
|
|
```
|
|
User: "Set up secure Stripe webhook handler"
|
|
Result: Complete webhook endpoint with signature verification, idempotency, and monitoring
|
|
```
|