今回は、Nest JSでStripeのWebhookを運用する際に必要となる署名検証について実装してみました。Webhookをセキュアに運用するために必要な知識について軽く触れて、具体的にNest JSで実装するまでを解説しています。公式のサンプルをNest JS用に手を加えてサンプルコード化してみました。
ご挨拶
どもども!ブログのコンテンツをバックエンドよりに寄せている龍ちゃんです。勉強のコストが上がっていて辛いところです。夢中になりすぎて、脱水にならないように水は飲みましょう。頭が痛くなったらとりあえず、水を飲むことが大切です。
今回もStripeのお話になります。Stripe APIを組み合わせる場合は、Webhookを扱うことがあります。それはもうびっくりするぐらいあります。業務フローと組み合わせることがあるならば、「処理の完了時にメールを送信する」みたいな要件もあるかもしれません。そんな要件をかなえるために素振りをしていきます。
このブログを読んでわかることとしては、以下になります。
- StripeでローカルWebhookを設定する方法
- WebhookをNestJSで検証する方法
それでは本編に入っていきます。
StripeのWebhookまわり
まずは、StripeでWebhookを設定する方法について解説していきます。その後、Webhookでセキュリティを保守する必要性について、NestJSで具体的に設定する方法について解説していきます。
StripeでWebhookを設定する方法
Webhookを設定する方法としては、インターネット上に設定する方法とローカルリスナーを登録する必要があります。オンライン上の設定をする場合は、こちらのページで簡単に設定することができます。今回は、ローカルイベントリスナーを設定する方法について解説していきます。こちらに関しては、コマンドを打ちこむだけで実行できるのが良いところです。
ローカルイベントリスナーを設定するためには、Stripe CLIが必要になります。設定の流れとしては、以下になります。
- Stripe CLIでログイン
- ローカルのWebhookに登録する
- CLIでイベントを送信する or 個別でAPIを実行して自力でイベントを送信
この流れで、対応するコマンドを実行する必要があります。
# 自動でポップアップが立ち上がります
stripe login
# Webhookにイベントを転送 localhost:4242は対応しているところに
stripe listen --forward-to localhost:4242/webhook
# 対応しているWebhookのイベント一覧を表示
stripe trigger --help
# paymentIntentが成功した場合のイベント
stripe trigger payment_intent.succeeded
対応しているイベントの種類としては、こちらにすべて記載されています。シナリオによってはWebhookを使用しない方法もあるので、そこはシナリオ次第ということで…
設定した際のミスについて書いておきます。
- コマンドプロンプトでしか実行されない
- Stripeのトリガーを送信した場合、整合性が取れるようにその他のイベントが実行される
一個目に関しては、そのまんまになります。GitBashで実行しようとして、動作せずに悲しい気持ちになっていました。
二個目に関しては、ちょっとイメージが難しいかもしれません。トリガーを送信した場合、そこに至るまで必要なイベントが実行されます。完了というイベントを送信するためには、作成と入力イベントが送信されます。トリガーをたくさん飛ばしてたら、知らないユーザーが自動で生成されるのでびっくりしないようにしておきましょう。
Webhookを保護する必要性について
それでは、徐々に本題によって行きたいと思います。今回は、StripeでWenhookを安全に運用するための検証を実装しました。なぜその検証が必要なのか?というお話をしていきます。以下に概念図を示します。
WebhookはStripeからイベントを受け取ります。その後、Webhook側からデータの更新などを送信します。もしWebhookが保護されていなければ、悪質な人がStripe側やDBのリソースをいじれてしまいます。
そのため、Webhookは送信者がStripeであることを確認して処理を実行する必要があります。この辺りは、公式のページでも詳しく解説してあります。今回は、Stripeから送信されてきたイベントをコードレベルで検証する方法を取り上げます。
Nest JSを用いてWebhookの検証を行う
本題がやってきました。StripeのWebhookから送信されてきたイベントを、Nest JSで検証する方法について解説していきます。実装については、以下の要素が必要になります。
署名検証用の関数は、StripeAPIのリファレンスに記載を見つけることができませんでしたが、公式サイトのほうには記載があったので、使用は問題ないかと思います。Nest JSでは、Guardという機能を使用して署名検証の処理を挟み込みます。
【Stripe】証明検証処理
ここでは、Stripeライブラリを用いて署名検証を実行する方法に関して解説しています。基本的な形としては、公式で解説されているコードと遜色ありません。
import { Injectable } from '@nestjs/common';
import { EnvironmentsService } from 'src/config/environments.service';
import Stripe from 'stripe';
@Injectable()
export class WebhookService {
private stripe: Stripe;
constructor() {
this.stripe = new Stripe("sk_xxxxxxxxxxxxxxxxxxxxxxxxxxx", {
apiVersion: '2023-08-16',
typescript: true,
});
}
async signatureVerification(payload: string | Buffer, sig: string, secret: string): Promise<boolean> {
try {
await this.stripe.webhooks.constructEvent(payload, sig, secret);
return true;
} catch (e) {
return false;
}
}
}
署名検証に必要な引数としては、生リクエスト(BufferまたはString)・ヘッダーに含まれている署名・Webhookのシークレットになります。Nest JSで生リクエストを取得する方法に関しては、次節で解説します。Webhookのシークレットは、ローカルリスナーを登録した際に、画面に表示されます。シークレットとしては、whsec_
から始まるIDになります。以下にサンプル画面を出しておきます。
Nest JSで生のリクエストを取得する方法
Nest CLIを用いてアプリを作成した場合、生のリクエストを取得することができません。(できるかもしれませんが…)その部分に関して、公式のリファレンスで解説がありました。main.tsにrawBody: true
の設定を追記してください。
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
rawBody: true,
bodyParser: true,
});
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
この設定で、生のリクエストを取得することができます。
Nest JSでGuardを使用してルート保護を行う
それでは、実際にGuardを用いて署名検証を挟み込む処理を実装していきます。
import { CanActivate, ExecutionContext, Injectable, RawBodyRequest } from '@nestjs/common';
import { Observable } from 'rxjs';
import { EnvironmentsService } from 'src/config/environments.service';
import { WebhookService } from 'src/webhook/webhook.service';
import Stripe from 'stripe';
@Injectable()
export class StripeSignatureVerificationGuard implements CanActivate {
constructor(private readonly service: WebhookService) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<RawBodyRequest<Request>>();
const body = request.rawBody;
const sig = request['headers']['stripe-signature'];
const webhookSecret = "whsec_xxxxxxxxxxxxxxxxxxxxx";
const event = request.body['type'] as Stripe.Event.Type;
if (!event) return false;
return this.service.signatureVerification(body, sig, webhookSecret);
}
}
ここでは、リクエストから生のリクエストと署名を抜き出し、環境変数からWebhookのシークレットを取得し、Webhookの署名検証に値を渡しています。
Guardの使用方法はControllerで@UseGuards(StripeSignatureVerificationGuard)
を呼び出すことで、署名検証の処理が走ります。
終わりに
以上で、「Nest JSでStripe Webhookの署名検証処理を挟み込む」実装は終了しました。検証系は、セキュアに運用していく上で必要なものですが、自分で対処できるところはしないといけません。これは、Stripeに限らずいろんなサービスで適応するべき内容です。
Stripeの記事も3本目です。
Nest JSの記事と併用して出していきます。
何もわからない民が「チョットワカル」を目指していきます。ではまた~**| ‾᷄ω‾᷅)و✧グッ**
Interesting! I’m going to have to try this out.