Sécurité des webhooks
Vérification de signature des webhooks Stripe, idempotence, gestion des erreurs et tests locaux.
Vue d'ensemble
L'Edge Function stripe-webhook reçoit les événements de Stripe chaque fois qu'un paiement, un abonnement ou une facture change. Comme cet endpoint est accessible publiquement, il doit vérifier que les requêtes proviennent réellement de Stripe.
Vérification de signature Stripe
Chaque requête webhook Stripe inclut un en-tête stripe-signature. L'Edge Function vérifie cette signature en utilisant votre secret webhook :
// supabase/functions/stripe-webhook/index.ts
import Stripe from "https://esm.sh/stripe@14?target=deno";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!, {
apiVersion: "2023-10-16",
httpClient: Stripe.createFetchHttpClient(),
});
const webhookSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET")!;
Deno.serve(async (req) => {
const signature = req.headers.get("stripe-signature");
if (!signature) {
return new Response("No signature", { status: 400 });
}
const body = await req.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
console.error("Webhook signature verification failed:", err.message);
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
// Signature verified -- process the event
// ...
return new Response(JSON.stringify({ received: true }), { status: 200 });
});Points clés :
- Lisez le body en texte brut (
req.text()), pas en JSON. Parser en JSON avant la vérification casse la vérification de signature. - Le
STRIPE_WEBHOOK_SECRETcommence parwhsec_. Obtenez-le depuis le dashboard Stripe lors de la création de l'endpoint webhook. - Si la vérification échoue, retournez
400immédiatement. Ne traitez pas l'événement.
Idempotence
Stripe peut envoyer le même événement plusieurs fois (par exemple lors de réessais). Votre handler doit être idempotent -- traiter le même événement deux fois ne doit pas causer de problèmes.
Stratégie : Utiliser l'ID de l'événement
// Check if we've already processed this event
const { data: existing } = await supabaseAdmin
.from("webhook_events")
.select("id")
.eq("stripe_event_id", event.id)
.single();
if (existing) {
// Already processed, skip
return new Response(JSON.stringify({ received: true }), { status: 200 });
}
// Process the event
await handleEvent(event);
// Record that we processed it
await supabaseAdmin
.from("webhook_events")
.insert({ stripe_event_id: event.id, type: event.type });Stratégie : Upsert au lieu d'insert
Pour les cas plus simples, utilisez upsert pour que les événements dupliqués écrasent simplement les données :
await supabaseAdmin
.from("subscriptions")
.upsert({
stripe_subscription_id: subscription.id,
user_id: userId,
status: subscription.status,
plan_id: planId,
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
}, {
onConflict: "stripe_subscription_id",
});Gestion des erreurs
Gérez les erreurs de manière élégante pour éviter de perdre des événements :
Deno.serve(async (req) => {
// ... signature verification ...
try {
switch (event.type) {
case "checkout.session.completed":
await handleCheckoutCompleted(event.data.object);
break;
case "customer.subscription.updated":
await handleSubscriptionUpdated(event.data.object);
break;
case "customer.subscription.deleted":
await handleSubscriptionDeleted(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return new Response(JSON.stringify({ received: true }), { status: 200 });
} catch (error) {
console.error(`Error processing ${event.type}:`, error);
// Return 500 so Stripe retries the event
return new Response(
JSON.stringify({ error: "Processing failed" }),
{ status: 500 }
);
}
});Codes de retour :
200: Événement traité avec succès. Stripe ne réessaiera pas.400: Requête invalide (signature invalide). Stripe ne réessaiera pas.500: Traitement échoué. Stripe réessaiera avec un backoff exponentiel (jusqu'à 3 jours).
Configurer le webhook
Dans le dashboard Stripe
- Allez dans Developers > Webhooks.
- Cliquez sur Add endpoint.
- Entrez l'URL de votre endpoint :
https://<project-ref>.supabase.co/functions/v1/stripe-webhook - Sélectionnez les événements à écouter :
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failed
- Copiez le Signing secret (
whsec_...). - Définissez-le dans Supabase :
pnpm supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_xxx
Tester les webhooks localement
Avec Stripe CLI
# Install Stripe CLI and login
stripe login
# Forward events to your local Supabase
stripe listen --forward-to http://localhost:54321/functions/v1/stripe-webhook
# In another terminal, trigger test events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failedLa Stripe CLI fournit un secret webhook temporaire pour les tests locaux. Utilisez-le dans votre supabase/.env :
STRIPE_WEBHOOK_SECRET=whsec_test_xxxVérifier dans le dashboard Stripe
Après le déploiement, consultez Developers > Webhooks > [votre endpoint] pour voir :
- Les livraisons d'événements récentes
- Les codes de réponse et les corps de réponse
- Les tentatives de réessai pour les livraisons échouées
Fini ? Marquez cette page comme terminée.