Práctica: Implementar un Endpoint de Webhook Seguro

Lectura
30 min~5 min lectura

Concepto clave

Los webhooks de Stripe son notificaciones HTTP que Stripe envía a tu servidor cuando ocurren eventos importantes en tu cuenta, como un pago exitoso, una suscripción cancelada o una factura generada. Imagina que son como mensajeros que llegan a tu puerta cada vez que algo relevante sucede en el sistema de pagos, permitiéndote reaccionar en tiempo real sin tener que consultar constantemente la API.

La seguridad es crítica porque estos webhooks contienen datos sensibles sobre transacciones y clientes. Un endpoint inseguro podría permitir que atacantes falsifiquen eventos o accedan a información confidencial. En la práctica, esto se protege verificando la firma digital que Stripe incluye en cada solicitud, similar a cómo un sello oficial autentica un documento importante.

Cómo funciona en la práctica

Cuando configuras un webhook en el dashboard de Stripe, proporcionas una URL de tu servidor (por ejemplo, https://tudominio.com/api/webhooks/stripe). Stripe enviará una solicitud POST a esta URL cada vez que ocurra un evento al que te hayas suscrito. Tu servidor debe:

  1. Recibir la solicitud y extraer el cuerpo (payload) y los encabezados
  2. Verificar la firma usando tu clave secreta de webhook
  3. Procesar el evento según su tipo
  4. Responder con un código de estado HTTP 200 para confirmar la recepción

Un flujo típico para una suscripción sería: 1) Cliente completa el pago inicial, 2) Stripe envía evento customer.subscription.created, 3) Tu endpoint recibe el evento, 4) Actualiza tu base de datos para activar el acceso del cliente.

Código en acción

Aquí tienes un ejemplo básico en Node.js usando Express:

const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();

// Middleware para parsear JSON
app.use(express.json());

app.post('/api/webhooks/stripe', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  
  try {
    // Verificar la firma del webhook
    const event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
    
    // Manejar diferentes tipos de eventos
    switch (event.type) {
      case 'customer.subscription.created':
        await handleSubscriptionCreated(event.data.object);
        break;
      case 'invoice.payment_succeeded':
        await handlePaymentSucceeded(event.data.object);
        break;
      case 'customer.subscription.deleted':
        await handleSubscriptionDeleted(event.data.object);
        break;
      default:
        console.log(`Evento no manejado: ${event.type}`);
    }
    
    res.json({received: true});
  } catch (err) {
    console.error('Error en webhook:', err.message);
    res.status(400).send(`Webhook Error: ${err.message}`);
  }
});

async function handleSubscriptionCreated(subscription) {
  // Aquí actualizarías tu base de datos
  console.log(`Suscripción creada: ${subscription.id}`);
  // Ejemplo: activar acceso del usuario en tu sistema
}

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Servidor escuchando en puerto ${PORT}`);
});

Y aquí una versión mejorada con manejo de errores más robusto:

// Versión mejorada con retry logic y logging
app.post('/api/webhooks/stripe', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const eventId = req.headers['stripe-event-id'] || 'unknown';
  
  // Log inicial para debugging
  console.log(`Recibiendo webhook ${eventId}`);
  
  try {
    const event = stripe.webhooks.constructEvent(
      req.rawBody || JSON.stringify(req.body),
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
    
    // Verificar que no hayamos procesado este evento antes
    if (await isEventProcessed(event.id)) {
      console.log(`Evento ${event.id} ya procesado, ignorando`);
      return res.json({received: true});
    }
    
    // Procesar en background para responder rápido a Stripe
    processEventAsync(event);
    
    // Marcar como procesado
    await markEventAsProcessed(event.id);
    
    res.json({received: true});
  } catch (err) {
    // Log detallado del error
    console.error(`Error en webhook ${eventId}:`, {
      error: err.message,
      signature: sig,
      body: req.body
    });
    
    // Diferenciar entre errores de firma y otros errores
    if (err.message.includes('Signature')) {
      res.status(401).send('Firma inválida');
    } else {
      res.status(400).send(`Error: ${err.message}`);
    }
  }
});

Errores comunes

  • No verificar la firma del webhook: Esto expone tu endpoint a ataques de falsificación. Siempre usa stripe.webhooks.constructEvent() o su equivalente en tu lenguaje.
  • Usar el mismo secret para diferentes entornos: Genera secrets separados para desarrollo, staging y producción en el dashboard de Stripe.
  • No manejar eventos duplicados: Stripe puede reenviar eventos. Implementa idempotencia almacenando IDs de eventos procesados.
  • Procesamiento síncrono largo: Si tu lógica tarda más de 10 segundos, Stripe podría reintentar. Procesa en background y responde inmediatamente.
  • No probar con eventos reales: Usa la CLI de Stripe o el modo test para enviar eventos de prueba antes de ir a producción.

Checklist de dominio

  1. ✅ Configuré un endpoint webhook en el dashboard de Stripe con la URL correcta
  2. ✅ Implementé verificación de firma usando el webhook secret
  3. ✅ Manejo al menos 3 tipos de eventos relevantes para mi negocio
  4. ✅ Mi endpoint responde en menos de 5 segundos para evitar timeouts
  5. ✅ Implementé idempotencia para evitar procesamiento duplicado
  6. ✅ Tengo logging adecuado para debugging de eventos
  7. ✅ Probé mi endpoint con la CLI de Stripe y eventos reales

Implementa un endpoint de webhook seguro para manejar suscripciones

En este ejercicio, crearás un endpoint de webhook seguro que maneje eventos críticos de suscripciones. Sigue estos pasos:

  1. Configura tu entorno: Crea un proyecto Node.js/Express básico e instala el SDK de Stripe (npm install stripe express). Configura las variables de entorno para STRIPE_SECRET_KEY y STRIPE_WEBHOOK_SECRET.
  2. Crea el endpoint: Implementa una ruta POST en /api/webhooks/stripe que:
    • Reciba solicitudes POST con JSON
    • Extraiga el header stripe-signature
    • Verifique la firma usando stripe.webhooks.constructEvent()
    • Responda con 200 si es válido, 401 si la firma es inválida
  3. Implementa manejo de eventos: Añade lógica para procesar estos eventos:
    • customer.subscription.created: Registrar en consola "Suscripción creada" + ID
    • invoice.payment_succeeded: Registrar "Pago exitoso" + monto
    • customer.subscription.deleted: Registrar "Suscripción cancelada" + razón
  4. Añade idempotencia: Implementa un sistema simple (puede ser en memoria) para evitar procesar el mismo evento dos veces.
  5. Prueba con la CLI: Instala la CLI de Stripe y ejecuta stripe listen --forward-to localhost:3000/api/webhooks/stripe para probar eventos reales.

Entrega: Código completo del endpoint y captura de pantalla mostrando eventos procesados correctamente.

Pistas
  • Recuerda que req.body debe ser el string RAW, no el objeto parseado, para la verificación de firma
  • Puedes usar un Set o un objeto simple en memoria para almacenar IDs de eventos procesados temporalmente
  • La CLI de Stripe necesita autenticación con stripe login antes de poder usarla

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.