Jak bezpečně zacházet s proužkovými webhooky

Jak přijímat a analyzovat webhooky Stripe, ověřovat jejich obsah a používat jejich data ve vaší aplikaci.

Začínáme

Pro tento tutoriál použijeme CheatCode Node.js Boilerplate jako výchozí bod pro naši práci. Pro začátek naklonujme kopii z Github:

Terminál

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

Dále cd do projektu a nainstalujte jeho závislosti:

Terminál

cd nodejs-server-boilerplate && npm install

Dále musíme přidat ještě jednu závislost stripe což nám pomůže analyzovat a ověřit webhooky, které dostáváme od Stripe:

Terminál

npm i stripe

Nakonec pokračujte a spusťte vývojový server:

Terminál

npm run dev

Díky tomu jsme připraveni začít.

Získání tajného klíče a podepisovacího tajemství webhooku

Než se pustíme do kódu, první věc, kterou musíme udělat, je získat přístup ke dvěma věcem:našemu Stripe Secret Key a našemu Webhooku Signing Secret.

Chcete-li je získat, budete muset mít existující účet Stripe. Pokud jej ještě nemáte, můžete se zaregistrovat zde. Jakmile budete mít přístup k ovládacímu panelu Stripe, můžete pokračovat níže uvedenými kroky.

Jakmile se přihlásíte, vyhledejte svůj tajný klíč:

  1. Nejprve se v pravém horním rohu ujistěte, že jste přepnuli přepínač "Testovací režim" tak, aby se rozsvítil (při psaní se tato barva po aktivaci změní na oranžovou).
  2. Vlevo od tohoto přepínače klikněte na tlačítko „Vývojáři“.
  3. Na další stránce v levé navigační nabídce vyberte kartu „Klíče API“.
  4. V bloku „Standardní klíče“ na této stránce vyhledejte svůj „Tajný klíč“ a klikněte na tlačítko „Odhalit testovací klíč“.
  5. Zkopírujte tento klíč (uchovávejte jej v bezpečí, protože se používá k provádění transakcí s vaším účtem Stripe).

Poté, jakmile budeme mít náš tajný klíč, musíme otevřít projekt, který jsme právě naklonovali, a přejít na /settings-development.json soubor:

/settings-development.json

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

export default settings;

V tomto souboru abecedně v dolní části exportovaného settings objekt, chceme přidat novou vlastnost stripe a nastavte jej na objekt s jedinou vlastností:secretKey . Pro hodnotu této vlastnosti chceme vložit tajný klíč, který jste zkopírovali z panelu Stripe výše. Vložte jej a poté tento soubor uložte.

Dále potřebujeme získat ještě jednu hodnotu:naše podpisové tajemství webhooku. K tomu musíme vytvořit nový koncový bod. Na stejné kartě „Vývojáři“ na hlavním panelu Stripe vyhledejte v levé navigaci (kde jste klikli na „klíče API“) možnost „Webhooks“.

Na této stránce buď uvidíte výzvu k vytvoření prvního koncového bodu webhooku, nebo možnost přidat další koncový bod. Kliknutím na možnost „Přidat koncový bod“ zobrazíte obrazovku konfigurace webhooku.

V okně, které se odkryje, chceme upravit pole „Adresa URL koncového bodu“ a poté vybrat události, které chceme na Stripe poslouchat.

V poli URL chceme použít název domény, kde naše aplikace běží. Pokud bychom byli například ve výrobě, mohli bychom udělat něco jako https://cheatcode.co/webhooks/stripe . V našem příkladu, protože předpokládáme, že naše aplikace běží na localhost, potřebujeme adresu URL, která ukazuje zpět na náš počítač.

K tomu se velmi doporučuje nástroj Ngrok. Je to bezplatná služba (s placenými možnostmi pro další funkce), která vám umožňuje vytvořit tunel zpět do vašeho počítače přes internet. Pro naše demo, https://tunnel.cheatcode.co/webhooks/stripe koncový bod, který používáme, ukazuje zpět na náš localhost přes Ngrok (bezplatné plány získají doménu na <randomId>.ngrok.io , ale placené plány mohou používat vlastní doménu, jako je tunnel.cheatcode.co ten, který zde používáme).

Důležitou částí je zde část za doménou:/webhooks/stripe . Toto je trasa, která je definována v rámci naše aplikace, kam očekáváme zasílání webhooků.

Dále, hned pod tím, chceme kliknout na tlačítko "Vybrat události" pod záhlavím "Vybrat události k poslechu". V tomto dalším okně nám Stripe dává možnost přizpůsobit, které události bude odesílat do našeho koncového bodu. Ve výchozím nastavení budou odesílat události všech typů, ale doporučuje se, abyste si to přizpůsobili pro potřeby vaší aplikace .

Pro naši ukázku přidáme dva typy událostí:invoice.payment_succeeded (odesláno vždy, když úspěšně obdržíme platbu od zákazníka) a invoice.payment_failed (odesláno vždy, když platba od zákazníka selže ).

Po přidání těchto – nebo jakýchkoli událostí, které preferujete – klikněte na tlačítko „Přidat koncový bod“.

A konečně, chcete-li získat své podpisové tajemství Webhooku, ze stránky zobrazené po vytvoření koncového bodu v řádku pod adresou URL vyhledejte pole „tajemství podpisu“ a klikněte na odkaz „Odhalit“ uvnitř něj. Zkopírujte tajemství, které bylo odhaleno.

/settings-development.json

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

Zpět ve vašem /settings-development.json soubor pod stripe objekt, který jsme přidali dříve, přidejte další vlastnost webhookSecret a nastavte hodnotu na tajný klíč, který jste právě zkopírovali z řídicího panelu Stripe.

Přidání middlewaru pro analýzu požadavku webhooku

Nyní jsme připraveni dostat se do kódu. Za prvé, abychom zajistili, že webhooky ze Stripe správně přijímáme, musíme se ujistit, že správně zpracováváme tělo požadavku, které od Stripe obdržíme.

Uvnitř projektu, který jsme naklonovali výše, budeme chtít přejít na /middleware/bodyParser.js soubor:

/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);
};

V tomto souboru najdeme existující middleware analýzy těla pro základní verzi. Zde najdete řadu podmíněných příkazů, které mění jak tělo požadavku by mělo být analyzováno v závislosti na původu požadavku a jeho specifikovaném Content-Type záhlaví (toto je mechanismus používaný v požadavku HTTP k označení formátu dat v poli těla požadavku).

Obecně řečeno, tělo požadavku bude obvykle odesláno jako data JSON nebo jako data zakódovaná ve formuláři URL. Tyto dva typy jsou již zpracovány v našem middlewaru.

Abychom mohli správně zpracovávat požadavky z Stripe, potřebujeme podporovat raw Tělo HTTP (toto je neanalyzovaný tělo požadavku HTTP, obvykle prostý text nebo binární data). Potřebujeme to pro Stripe, protože to očekávají od své vlastní funkce validátoru webhooku (na co se podíváme později).

Do výše uvedeného kódu přidáme další if pro kontrolu záhlaví HTTP stripe-signature na všechny příchozí požadavky do naší aplikace. Funkce exportovaná výše je volána přes /middleware/index.js soubor, který je sám volán předtím, než je příchozí požadavek předán našim trasám v /index.js pro rozlišení.

Pokud vidíme HTTP hlavičku stripe-signature , víme, že dostáváme příchozí požadavek od Stripe (webhook) a že chceme zajistit, aby tělo tohoto požadavku zůstalo v nezpracovaném stavu. K tomu zavoláme .raw() metoda na bodyParser objekt importovaný v horní části našeho souboru (knihovna, která nabízí kolekci funkcí specifických pro formát pro formátování dat těla požadavku HTTP).

Tomu předáme objekt options, který říká, že chceme povolit jakékoli */* datový typ a nastavte limit velikosti těla požadavku na 50mb . To zajišťuje, že náklad jakékoli velikosti může projít bez spouštění jakýchkoli chyb (neváhejte si s tím hrát podle svých vlastních potřeb).

Konečně, protože očekáváme .raw() metodu vrátit funkci, okamžitě tuto funkci zavoláme a předáme req , res a next argumenty předávané nám přes Express, když volá náš middleware.

Díky tomu jsme připraveni ponořit se do skutečných ovladačů pro naše webhooky. Nejprve musíme přidat /webhooks/stripe koncový bod, o kterém jsme se zmiňovali dříve při přidávání našeho koncového bodu na řídicí panel Stripe.

Přidání expresního koncového bodu pro příjem webhooků

Tenhle je rychlý. Připomeňme, že dříve jsme na panelu Stripe přiřadili náš koncový bod http://tunnel.cheatcode.co/webhooks/stripe . Nyní musíme přidat /webhooks/stripe trasu v naší aplikaci a propojte ji s kódem obslužného programu, který bude analyzovat a přijímat naše webhooky.

/api/index.js

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

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

Výše uvedená funkce, kterou exportujeme, je volána prostřednictvím našeho /index.js soubor za middleware() funkce. Tato funkce je určena k nastavení API nebo tras pro naši aplikaci. Ve výchozím nastavení je v tomto standardu naše API založeno na GraphQL. graphql() volání funkce, které zde vidíme, je irelevantní, ale app argument, který dostává, je důležitý.

Toto je Express app instance vytvořená v našem /index.js soubor. Zde chceme zavolat na .post() metodu na této instanci aplikace sdělit Express, že bychom chtěli definovat trasu, která obdrží požadavek HTTP POST (co očekáváme, že dostaneme od Stripe). Aby náš kód zůstal otevřený a byl použitelný pro Stripe i další služby, definujeme adresu URL naší trasy jako /webhooks/:service kde :service je parametr, který lze zaměnit za název libovolné služby (např. /webhooks/stripe nebo /webhooks/facebook ).

Dále se chceme podívat na funkci uloženou v webhooks proměnnou, kterou importujeme v horní části souboru a předáváme ji jako druhý argument naší cesty.

Přidání obslužného programu webhooku

Skutečným základem naší implementace bude funkce handleru, kterou nyní napíšeme. Zde dosáhneme dvou věcí:

  1. Ověření datové části webhooku, kterou dostáváme ze služby Stripe (aby bylo zajištěno, že data, která přijímáme, skutečně pocházejí z Proužek).
  2. Vyhledání a volání příslušného kódu (funkce) na základě typu webhooku (pro náš příklad buď invoice.payment_succeeded nebo invoice.payment_failed ).

Pro začátek napíšeme ověřovací kód pomocí stripe balíček, který jsme dříve nainstalovali:

/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.");
  }
};

V našem předchozím kroku jsme nastavili expresní trasu a předali jí proměnnou webhooks , funkce, jako druhý argument, který je volán při požadavku na vámi definovanou adresu URL, v tomto případě /webhooks/stripe .

Ve výše uvedeném kódu exportujeme funkci, která má tři argumenty:req , res a next . Očekáváme tyto konkrétní argumenty, protože to jsou to, co Express předá funkci zpětného volání pro trasu (v tomto případě je tato funkce zpětného volání funkce, kterou zde exportujeme a importujeme zpět v /api/index.js jako webhooks ).

Uvnitř této funkce musíme potvrdit, že služba, kterou přijímáme, žádá o stripe má odpovídající funkci handleru, která jej podporuje. Je to proto, abychom z internetu nedostávali náhodné požadavky (např. někdo spamující /webhooks/hotdog nebo /webhooks/pizzahut ).

Abychom ověřili, že _máme _ funkci handleru, nad naší exportovanou funkcí jsme definovali objekt handlers a definovali Stripe jako funkci on tento objekt (funkce definovaná na objektu je v JavaScriptu označována jako metoda).

U této metody očekáváme, že přijmeme objekt požadavku HTTP předaný naší trase. Zpět v naší exportované funkci – zpětném volání trasy – určíme, který obslužný program zavolat na základě req.params.service hodnota. Pamatujte, :service v naší adrese URL může být cokoliv, takže se musíme ujistit, že nejprve existuje než to zavoláte. K tomu používáme JavaScriptovou závorkovou notaci, která říká „na handlers objekt, zkuste najít vlastnost s názvem rovným hodnotě req.params.service ."

V našem příkladu bychom očekávali handlers.stripe být definován. Pokud je to handler existuje, chceme zpětně upozornit na původní požadavek, že byl webhook přijat, a poté zavolat že handler() funkce, předáním req které chceme zvládnout.

/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.");
  }
};

Vyplňte naše stripe() funkci handleru, než uděláme cokoliv s webhookem, který jsme obdrželi od Stripe, chceme zajistit, aby webhook, který dostáváme, skutečně pocházel z ze Stripe a ne někdo, kdo se nám snaží poslat podezřelá data.

K tomu nám Stripe poskytuje šikovnou funkci ve své knihovně Node.js – stripe balíček, který jsme nainstalovali na začátku tutoriálu — pro provedení této úlohy:stripe.webhooks.constructEvent() .

Zde importujeme instanci stripe ze souboru /stripe/index.js umístěn uvnitř našeho stávajícího /api/webhooks složku (nastavíme ji v další sekci, takže prozatím předpokládáme její existenci).

Očekáváme, že tato instance bude objekt obsahující .webhooks.constructEvent() funkce, kterou zde voláme. Tato funkce očekává tři argumenty:

  1. Číslo request.body které jsme obdrželi v požadavku HTTP POST od Stripe.
  2. Číslo stripe-signature záhlaví z požadavku HTTP POST, který jsme obdrželi od Stripe.
  3. Naše webhookSecret který jsme nastavili a přidali do našeho /settings-development.json soubor dříve.

První dva argumenty máme okamžitě k dispozici prostřednictvím HTTP request (nebo req jak jsme na něj odkazovali jinde), objekt, který jsme obdrželi od Stripe. Pro webhookSecret , importovali jsme náš soubor nastavení jako settings v horní části našeho souboru s využitím vestavěné funkce načítání nastavení v /lib/settings.js abychom pro nás vybrali správná nastavení na základě našeho aktuálního prostředí (na základě hodnoty process.env.NODE_ENV , například development nebo production ).

Uvnitř constructEvent() , Stripe se pokusí porovnat stripe-signature záhlaví s hašovanou kopií přijatého request.body . Myšlenka je taková, že pokud je tento požadavek platný, podpis je uložen v stripe-signature se bude rovnat hašované verzi request.body pomocí našeho webhookSecret (možné pouze v případě, že používáme platný webhookSecret a přijetí legitimního požadavku od Stripe).

Pokud dělají shodu, očekáváme data proměnnou, kterou přiřadíme .constructEvent() call to obsahuje webhook, který jsme obdrželi od Stripe. Pokud naše ověření selže, očekáváme, že toto bude prázdné.

Pokud je prázdné, vrátíme null z našeho stripe() funkce (toto je čistě symbolické, protože od naší funkce neočekáváme návratovou hodnotu).

Za předpokladu, že jsme úspěšně přijali nějaká data, chceme dále zkusit najít obslužný program webhooku pro konkrétní type události, kterou dostáváme od Stripe. Zde očekáváme, že bude k dispozici v type vlastnost na data objekt.

V horní části našeho souboru také předpokládáme, že naše /stripe/index.js soubor zde v /api/webhooks bude obsahovat exportovanou hodnotu webhooks který jsme přejmenovali na stripeWebhooks při importu nahoru (opět jsme to ještě nevytvořili – pouze předpokládáme, že existuje).

U tohoto objektu, jak uvidíme v další části, očekáváme vlastnost odpovídající názvu webhooku type jsme obdrželi (např. invoice.payment_succeeded nebo invoice.payment_failed ).

Pokud ano existuje, očekáváme, že nám vrátí funkci, která sama očekává, že obdrží data obsažená v našem webhooku. Za předpokladu, že ano, nazýváme to handler() funkce, předávání data.data.object —zde pomocí volitelného řetězení JavaScriptu, aby bylo zajištěno, že object existuje na data objekt nad ním, který existuje na data objekt jsme uložili analyzované a ověřené tělo požadavku z Stripe.

Na závěr se podívejme na tento /api/webhooks/stripe/index.js soubor, kolem kterého jsme tančili.

Přidávání funkcí pro zpracování konkrétních událostí webhooku

Nyní se podívejme, jak hodláme získat přístup k instanci Stripe, o které jsme se zmiňovali výše, a jak zacházet s každým z našich webhooků:

/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);

Zaměříme-li se na konec našeho souboru, zde vidíme stripe hodnotu, kde jsme nazvali stripe.webhooks.constructEvent() probíhá inicializace. Zde vezmeme Stripe funkce importovaná z stripe balíček, který jsme nainstalovali na začátku volaného tutoriálu a předali secretKey jsme převzali z ovládacího panelu Stripe a přidali do našeho /settings-development.json soubor dříve.

Nad tím můžeme vidět webhooks objekt, který jsme importovali a přejmenovali na stripeWebhooks zpět v /api/webhooks/index.js . Na něm máme dva typy událostí, které bychom rádi podporovali invoice.payment_succeeded a invoice.payment_failed definované, pro každé předání funkce s názvem odpovídajícím kódu, který chceme spustit, když obdržíme tyto specifické typy událostí.

Prozatím je každá z těchto funkcí omezena na export funkce console.log() je webhook, který jsme obdrželi od Stripe. Zde bychom chtěli využít webhook a provést změnu v naší databázi, vytvořit kopii faktury, kterou jsme obdrželi, nebo spustit některé další funkce v naší aplikaci.

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

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

A je to! Nyní rozprostřeme tunel pomocí nástroje Ngrok, který jsme naznačili dříve, a obdržíme testovací webhook od Stripe.

Zabalení

V tomto tutoriálu jsme se naučili, jak nastavit koncový bod webhooku na Stripe, získat tajemství webhooku a poté bezpečně ověřit webhook pomocí stripe.webhooks.constructEvent() funkce. Abychom se tam dostali, nastavili jsme cestu HTTP POST v Express a zapojili řadu funkcí, které nám pomohou organizovat naše obslužné nástroje webhooku na základě typu události přijaté z Stripe.