Cómo manejar de forma segura los webhooks de Stripe

Cómo recibir y analizar webhooks de Stripe, validar su contenido y usar sus datos en su aplicación.

Primeros pasos

Para este tutorial, vamos a utilizar CheatCode Node.js Boilerplate como punto de partida para nuestro trabajo. Para empezar, clonemos una copia de Github:

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate

A continuación, cd en el proyecto e instalar sus dependencias:

Terminal

cd nodejs-server-boilerplate && npm install

A continuación, debemos agregar una dependencia más stripe que nos ayudará a analizar y autenticar los webhooks que recibimos de Stripe:

Terminal

npm i stripe

Finalmente, continúe e inicie el servidor de desarrollo:

Terminal

npm run dev

Con eso, estamos listos para comenzar.

Obtener una clave secreta y un secreto de firma de webhook

Antes de profundizar en el código, lo primero que debemos hacer es obtener acceso a dos cosas:nuestra clave secreta de Stripe y nuestro secreto de firma de Webhook.

Para obtenerlos, deberá tener una cuenta de Stripe existente. Si aún no tienes uno, puedes registrarte aquí. Una vez que tenga acceso al panel de control de Stripe, puede continuar con los pasos a continuación.

Una vez que haya iniciado sesión, para localizar su clave secreta:

  1. Primero, en la esquina superior derecha, asegúrese de haber activado el botón "Modo de prueba" para que se ilumine (en el momento de escribir esto, se volverá naranja cuando esté activado).
  2. A la izquierda de ese interruptor, haga clic en el botón "Desarrolladores".
  3. En la página siguiente, en el menú de navegación de la izquierda, seleccione la pestaña "Claves API".
  4. Bajo el bloque "Claves estándar" en esta página, busque su "Clave secreta" y haga clic en el botón "Revelar clave de prueba".
  5. Copie esta clave (guárdela en un lugar seguro ya que se usa para realizar transacciones con su cuenta de Stripe).

Luego, una vez que tengamos nuestra clave secreta, debemos abrir el proyecto que acabamos de clonar y navegar hasta el /settings-development.json archivo:

/configuración-desarrollo.json

const settings = {
  "authentication": { ... },
  "databases": { ... },
  "smtp": { ... },
  "stripe": {
    "secretKey": "<Paste your secret key here>"
  },
  "support": { ... },
  "urls": { ... }
};

export default settings;

En este archivo, alfabéticamente cerca de la parte inferior del settings exportado objeto, queremos agregar una nueva propiedad stripe y configúrelo en un objeto con una sola propiedad:secretKey . Para el valor de esta propiedad, queremos pegar la clave secreta que copió del panel de Stripe anterior. Péguelo y luego guarde este archivo.

A continuación, necesitamos obtener un valor más:nuestro secreto de firma de webhook. Para hacer esto, necesitamos crear un nuevo punto final. Desde la misma pestaña "Desarrolladores" en el panel de control de Stripe, desde la barra de navegación de la izquierda (donde hiciste clic en "Claves API"), busca la opción "Webhooks".

En esta página, verá un aviso para crear su primer punto final de webhook o la opción para agregar otro punto final Haga clic en la opción "Agregar punto final" para mostrar la pantalla de configuración del webhook.

En la ventana que aparece, queremos personalizar el campo "URL de punto final" y luego seleccionar los eventos que queremos escuchar de Stripe.

En el campo URL, queremos usar el nombre de dominio donde se ejecuta nuestra aplicación. Por ejemplo, si estuviéramos en producción, podríamos hacer algo como https://cheatcode.co/webhooks/stripe . Para nuestro ejemplo, debido a que anticipamos que nuestra aplicación se ejecutará en localhost, necesitamos una URL que apunte a nuestra máquina.

Para ello, la herramienta Ngrok es muy recomendable. Es un servicio gratuito (con opciones pagas para funciones adicionales) que le permite crear un túnel de regreso a su computadora a través de Internet. Para nuestra demostración, el https://tunnel.cheatcode.co/webhooks/stripe El punto final que estamos usando apunta a nuestro host local a través de Ngrok (los planes gratuitos obtienen un dominio en <randomId>.ngrok.io , pero los planes pagos pueden usar un dominio personalizado como el tunnel.cheatcode.co uno que estamos usando aquí).

La parte importante aquí es la parte después del dominio:/webhooks/stripe . Esta es la ruta que está definida dentro nuestra aplicación donde esperamos que se envíen los webhooks.

A continuación, justo debajo de esto, queremos hacer clic en el botón "Seleccionar eventos" debajo del encabezado "Seleccionar eventos para escuchar". En la siguiente ventana, Stripe nos da la opción de personalizar qué eventos enviará a nuestro punto final. Por defecto, enviarán eventos de todos tipos, pero se recomienda que lo personalice según las necesidades de su aplicación .

Para nuestra demostración, agregaremos dos tipos de eventos:invoice.payment_succeeded (enviado cada vez que recibimos con éxito un pago de un cliente) y invoice.payment_failed (enviado cada vez que un pago de un cliente falla ).

Una vez que haya agregado estos, o los eventos que prefiera, haga clic en el botón "Agregar punto final".

Finalmente, para obtener su secreto de firma de Webhook, desde la página que se muestra después de crear su punto final, en la fila debajo de la URL, ubique el cuadro "Secreto de firma" y haga clic en el enlace "Revelar" dentro de él. Copia el secreto que se revela.

/configuración-desarrollo.json

...
  "stripe": {
    "secretKey": "",
    "webhookSecret": "<Paste your secret here>"
  },
  ...
}

Vuelve a tu /settings-development.json archivo, bajo el stripe objeto que agregamos anteriormente, agregue una propiedad adicional webhookSecret y establezca el valor en el secreto que acaba de copiar del panel de control de Stripe.

Agregando middleware para analizar la solicitud de webhook

Ahora estamos listos para entrar en el código. En primer lugar, para asegurarnos de que recibimos correctamente los webhooks de Stripe, debemos asegurarnos de que estamos gestionando correctamente el cuerpo de la solicitud que recibiremos de Stripe.

Dentro del proyecto que clonamos arriba, querremos navegar al /middleware/bodyParser.js archivo:

/middleware/bodyParser.js

import bodyParser from "body-parser";

export default (req, res, next) => {
  const contentType = req.headers["content-type"];

  if (req.headers["stripe-signature"]) {
    return bodyParser.raw({ type: "*/*", limit: "50mb" })(req, res, next);
  }
  
  if (contentType && contentType === "application/x-www-form-urlencoded") {
    return bodyParser.urlencoded({ extended: true })(req, res, next);
  }

  return bodyParser.json()(req, res, next);
};

En este archivo, encontraremos el middleware del analizador de cuerpo existente para el repetitivo. Aquí encontrará una serie de declaraciones condicionales que cambian cómo el cuerpo de la solicitud debe analizarse según el origen de la solicitud y su Content-Type especificado encabezado (este es el mecanismo utilizado en una solicitud HTTP para designar el formato de los datos en el campo del cuerpo de una solicitud).

En términos generales, el cuerpo de la solicitud normalmente se enviará como datos JSON o como datos codificados de formulario de URL. Estos dos tipos ya se manejan en nuestro middleware.

Para gestionar correctamente las solicitudes de Stripe, debemos admitir un sin procesar Cuerpo HTTP (este es el sin analizar Cuerpo de la solicitud HTTP, generalmente texto sin formato o datos binarios). Necesitamos esto para Stripe, ya que esto es lo que esperan de su propia función de validación de webhook (lo que veremos más adelante).

En el código anterior, agregamos un if adicional declaración para verificar un encabezado HTTP stripe-signature en todas las solicitudes entrantes a nuestra aplicación. La función exportada arriba se llama a través de /middleware/index.js archivo que se llama antes de que se transfiera una solicitud entrante a nuestras rutas en /index.js para la resolución.

Si vemos el encabezado HTTP stripe-signature , sabemos que estamos recibiendo una solicitud entrante de Stripe (un webhook) y que queremos asegurarnos de que el cuerpo de esa solicitud permanezca en su estado original. Para hacerlo llamamos al .raw() método en el bodyParser objeto importado en la parte superior de nuestro archivo (una biblioteca que ofrece una colección de funciones específicas de formato para formatear los datos del cuerpo de la solicitud HTTP).

A él, le pasamos un objeto de opciones que dice que queremos permitir cualquier */* tipo de datos y establezca el límite de tamaño del cuerpo de la solicitud en 50mb . Esto garantiza que una carga útil de cualquier tamaño pueda pasar sin desencadenar ningún error (siéntete libre de jugar con esto según tus propias necesidades).

Finalmente, porque esperamos el .raw() para devolver una función, inmediatamente llamamos a esa función, pasando el req , res y next los argumentos que se nos pasan a través de Express cuando llama a nuestro middleware.

Con esto, estamos listos para profundizar en los controladores reales de nuestros webhooks. Primero, necesitamos agregar el /webhooks/stripe punto final al que nos referimos anteriormente al agregar nuestro punto final en el panel de control de Stripe.

Agregar un punto final Express para recibir webhooks

Este es rápido. Recuerde que anteriormente, en el panel de control de Stripe, asignamos nuestro punto final a http://tunnel.cheatcode.co/webhooks/stripe . Ahora, necesitamos agregar ese /webhooks/stripe ruta en nuestra aplicación y conéctelo al código del controlador que analizará y recibirá nuestros webhooks.

/api/index.js

import graphql from "./graphql/server";
import webhooks from "./webhooks";

export default (app) => {
  graphql(app);
  app.post("/webhooks/:service", webhooks);
};

Arriba, la función que estamos exportando se llama a través de nuestro /index.js archivo después del middleware() función. Esta función está diseñada para configurar la API o rutas para nuestra aplicación. De forma predeterminada, en este modelo, nuestra API se basa en GraphQL. El graphql() La llamada a la función que vemos aquí es irrelevante, pero el app el argumento que está recibiendo es importante.

Este es el Expreso app instancia creada en nuestro /index.js expediente. Aquí, queremos llamar al .post() en esa instancia de la aplicación para decirle a Express que nos gustaría definir una ruta que reciba una solicitud HTTP POST (lo que esperamos obtener de Stripe). Aquí, para mantener nuestro código abierto y aplicable a Stripe, así como a otros servicios, definimos la URL de nuestra ruta como /webhooks/:service donde :service es un parámetro que se puede intercambiar con el nombre de cualquier servicio (por ejemplo, /webhooks/stripe o /webhooks/facebook ).

A continuación, queremos echar un vistazo a la función almacenada en el webhooks variable que estamos importando en la parte superior del archivo y pasando como segundo argumento a nuestra ruta.

Agregar un controlador de webhook

La verdadera esencia de nuestra implementación será la función de controlador que vamos a escribir ahora. Aquí es donde lograremos dos cosas:

  1. Validación de la carga útil del webhook que recibimos de Stripe (para garantizar que los datos que recibimos sean realmente de) raya).
  2. Ubicar y llamar al código apropiado (una función) según el tipo de webhook (para nuestro ejemplo, ya sea invoice.payment_succeeded o invoice.payment_failed ).

Para empezar, vamos a escribir el código de validación usando el stripe paquete que instalamos anteriormente:

/api/webhooks/index.js

import _ from "lodash";
import settings from "../../lib/settings";
import { stripe } from "./stripe";

const handlers = {
  stripe(request) {
    // We'll implement our validation here.
  },
};

export default async (req, res, next) => {
  const handler = handlers[req.params.service];

  if (handler) {
    res.status(200).send("[200] Webhook received.");
    handler(req);
  } else {
    res.status(200).send("[200] Webhook received.");
  }
};

En nuestro paso anterior, configuramos una ruta Express, pasándole una variable webhooks , una función, como el segundo argumento que se llama cuando se realiza una solicitud a la URL que defina, en este caso /webhooks/stripe .

En el código anterior, estamos exportando una función que toma tres argumentos:req , res y next . Estamos anticipando estos argumentos específicos ya que estos son los que Express pasará a la función de devolución de llamada para una ruta (en este caso, esa función de devolución de llamada es la función que estamos exportando aquí e importando nuevamente en /api/index.js como webhooks ).

Dentro de esa función, debemos confirmar que el servicio que estamos recibiendo solicita stripe tiene una función de controlador correspondiente para admitirlo. Esto es para que no recibamos solicitudes aleatorias de Internet (por ejemplo, alguien enviando spam /webhooks/hotdog o /webhooks/pizzahut ).

Para verificar que _tenemos _ una función de controlador, arriba de nuestra función exportada hemos definido un objeto handlers y he definido Stripe como una función on ese objeto (una función definida en un objeto se denomina método en JavaScript).

Para ese método, esperamos recibir el objeto de solicitud HTTP pasado a nuestra ruta. Volviendo a nuestra función exportada, la devolución de llamada de ruta, determinamos a qué controlador llamar en función del req.params.service valor. Recuerda, el :service en nuestra URL puede ser cualquier cosa, por lo que debemos asegurarnos de que existe primero antes de llamarlo. Para hacer eso, usamos la notación de corchetes de JavaScript para decir "en el handlers objeto, intente encontrar una propiedad con un nombre igual al valor de req.params.service ."

Para nuestro ejemplo, esperaríamos handlers.stripe por definir Si ese handler existe, queremos señalar a la solicitud original que se recibió el webhook y luego llamar que handler() función, pasando el req que queremos manejar.

/api/webhooks/index.js

import _ from "lodash";
import settings from "../../lib/settings";
import { webhooks as stripeWebhooks, stripe } from "./stripe";

const handlers = {
  stripe(request) {
    const data = stripe.webhooks.constructEvent(
      request.body,
      request.headers["stripe-signature"],
      settings.stripe.webhookSecret
    );

    if (!data) return null;

    const handler = stripeWebhooks[data.type];

    if (handler && typeof handler === "function") {
      return handler(data?.data?.object);
    }

    return `${data.type} is not supported.`;
  },
};

export default async (req, res, next) => {
  const handler = handlers[req.params.service];
  if (handler) {
    res.status(200).send("[200] Webhook received.");
    handler(req);
  } else {
    res.status(200).send("[200] Webhook received.");
  }
};

Rellenando nuestro stripe() función de controlador, antes de hacer cualquier cosa con el webhook que recibimos de Stripe, queremos asegurarnos de que el webhook que recibimos sea realmente de Stripe y no alguien que intenta enviarnos datos sospechosos.

Para hacer eso, Stripe nos brinda una función útil en su biblioteca Node.js:el stripe paquete que instalamos al principio del tutorial—para realizar esta tarea:stripe.webhooks.constructEvent() .

Aquí, estamos importando una instancia de stripe del archivo /stripe/index.js ubicado dentro de nuestro /api/webhooks existente carpeta (la configuraremos en la siguiente sección, así que por ahora asumimos su existencia).

Esperamos que esa instancia sea un objeto que contenga el .webhooks.constructEvent() función a la que estamos llamando aquí. Esa función espera tres argumentos:

  1. El request.body que recibimos en la solicitud HTTP POST de Stripe.
  2. El stripe-signature encabezado de la solicitud HTTP POST que recibimos de Stripe.
  3. Nuestro webhookSecret que configuramos y añadimos a nuestro /settings-development.json archivo anterior.

Los dos primeros argumentos están inmediatamente disponibles para nosotros a través de HTTP request (o req como lo mencionamos en otro lugar) objeto que recibimos de Stripe. Para el webhookSecret , hemos importado nuestro archivo de configuración como settings en la parte superior de nuestro archivo, aprovechando la función de carga de configuración integrada en /lib/settings.js para seleccionar la configuración correcta para nosotros en función de nuestro entorno actual (basado en el valor de process.env.NODE_ENV , por ejemplo, development o production ).

Dentro de constructEvent() , Stripe intenta comparar el stripe-signature encabezado con una copia codificada del request.body recibido . La idea aquí es que, si esta solicitud es válida, la firma almacenada en stripe-signature será igual a la versión hash del request.body usando nuestro webhookSecret (solo es posible si estamos usando un webhookSecret válido y recibir una solicitud legítima de Stripe).

Si lo hacen coincidencia, esperamos el data variable que estamos asignando a nuestro .constructEvent() llame a para contener el webhook que recibimos de Stripe. Si nuestra validación falla, esperamos que esté vacío.

Si es vacío, devolvemos null de nuestro stripe() función (esto es puramente simbólico ya que no esperamos un valor de retorno de nuestra función).

Suponiendo que recibimos con éxito algunos datos, a continuación, queremos intentar encontrar el controlador de webhook para el type específico del evento que recibimos de Stripe. Aquí, esperamos que esté disponible en el type propiedad en el data objeto.

En la parte superior de nuestro archivo, también asumimos que nuestro /stripe/index.js archivo aquí en /api/webhooks contendrá un valor exportado webhooks que hemos renombrado como stripeWebhooks al importarlo en la parte superior (nuevamente, aún no lo hemos creado, solo asumimos que existe).

En ese objeto, como veremos en la siguiente sección, esperamos una propiedad que coincida con el nombre del webhook type hemos recibido (por ejemplo, invoice.payment_succeeded o invoice.payment_failed ).

Si lo hace existe, esperamos que nos devuelva una función que espera recibir los datos contenidos en nuestro webhook. Suponiendo que lo haga, lo llamamos handler() función, pasando data.data.object —aquí, usando el encadenamiento opcional de JavaScript para asegurar que object existe en el data objeto encima de él, que existe en el data objeto almacenamos el cuerpo de solicitud analizado y validado de Stripe.

Para concluir, echemos un vistazo a este /api/webhooks/stripe/index.js archivo con el que hemos estado bailando.

Adición de funciones para manejar eventos de webhook específicos

Ahora, veamos cómo pretendemos obtener acceso a la instancia de Stripe a la que aludimos anteriormente y manejar cada uno de nuestros webhooks:

/api/webhooks/stripe/index.js

import Stripe from "stripe";
import settings from "../../../lib/settings";

import invoicePaymentSucceeded from "./invoice.payment_succeeded";
import invoicePaymentFailed from "./invoice.payment_failed";

export const webhooks = {
  "invoice.payment_succeeded": invoicePaymentSucceeded,
  "invoice.payment_failed": invoicePaymentFailed,
};

export const stripe = Stripe(settings.stripe.secretKey);

Centrándonos en la parte inferior de nuestro archivo, aquí podemos ver el stripe valor donde llamamos al stripe.webhooks.constructEvent() siendo inicializado. Aquí, tomamos el Stripe función importada del stripe paquete que instalamos al comienzo del tutorial llamado, pasando el secretKey tomamos del tablero de Stripe y agregamos a nuestro /settings-development.json archivo anterior.

Encima de esto, podemos ver el webhooks objeto que importamos y renombramos como stripeWebhooks de vuelta en /api/webhooks/index.js . En él, tenemos los dos tipos de eventos que nos gustaría admitir invoice.payment_succeeded y invoice.payment_failed definido, para cada paso una función con un nombre correspondiente al código que queremos ejecutar cuando recibamos esos tipos específicos de eventos.

Por ahora, cada una de esas funciones se limita a exportar una función que console.log() Es el webhook que recibimos de Stripe. Aquí es donde nos gustaría tomar el webhook y hacer un cambio en nuestra base de datos, crear una copia de la factura que recibimos o activar alguna otra funcionalidad en nuestra aplicación.

/api/webhooks/stripe/invoice.payment_succeeded.js

export default (webhook) => {
  console.log(webhook);
};

¡Eso es todo! Ahora, hagamos girar un túnel a través de la herramienta Ngrok que mencionamos anteriormente y recibamos un webhook de prueba de Stripe.

Terminando

En este tutorial, aprendimos cómo configurar un punto final de webhook en Stripe, obtener un secreto de webhook y luego validar de forma segura un webhook usando el stripe.webhooks.constructEvent() función. Para llegar allí, configuramos una ruta HTTP POST en Express y conectamos una serie de funciones para ayudarnos a organizar nuestros controladores de webhook según el tipo de evento recibido de Stripe.