2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Stripeによる署名検証をFirebase Cloud Functionsを利用してローカルで行う

Posted at

Motivation

最近流行の決済サービスであるStripeであるが、決済処理時のエンドポイントに送信するWebhookイベントのヘッダに署名を含めることができる。これによりそれがサードパーティでなくStripeによって送信されたものであるかどうかを検証することができる。
そこで、本記事ではこの署名検証処理をローカルで手軽にテストできるようにすることを目的とする。

Prerequisite

テスト環境

OS: macOS
node: v14.17.3

Firebase Cloud Functions

プロジェクトの追加やCLIでのログインは省略
詳しくはこちら:https://firebase.google.com/docs/functions/get-started

Cloud Functionsの実装
index.ts
import * as express from "express";
import * as functions from "firebase-functions";
import { https } from "firebase-functions";
import Stripe from "stripe";

// 環境変数
const SECRET_KEY = "sk_xxxx"; // APIキー
const ENDPOINT_SECRET = "whsec_XXXX"; // 署名シークレット。後述のStripe CLIによるサーバー起動時に表示されるシークレットを指定する。

const app = express();

// webhook
app.post("/webhook-stripe/", async (req, res) => {
  const signature = req.headers["stripe-signature"];// ここに署名が入っている
  if (!signature) {
    throw new Error("no avalilable signature header");
  }

  try {
    const stripe = new Stripe(SECRET_KEY, {
      apiVersion: "2020-08-27",
    });

    // 署名検証
    const firebaseRequest = req as https.Request;
    const event = stripe.webhooks.constructEvent(firebaseRequest.rawBody, signature, ENDPOINT_SECRET);

    res.status(200).send();
  } catch (e) {
    console.log(e);
    res.status(500).send(`Verification Error: ${e.message}`);
  }
});

export const verifySignature = functions.https.onRequest(app);
ハマったポイント

stripe.webhooks.constructEvent()の第一引数に渡すpayloadは、stripeからのraw request body(Buffer)を渡さなければいけないが、通常の方法だとなぜか上手くいかず「No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe?」のエラーが出た。
原因:Cloud Functions + Expressの環境では、requestがデフォルトでjsonパースされているみたいで、どうしてもraw bodyが取得できなかった
解決法:

import { https } from "firebase-functions";
...
const firebaseRequest = req as https.Request;
// -> firebaseRequest.rawBodyをstripe.webhooks.constructEventのpayloadで渡すようにする
ローカルでエミュレート
$ firebase emulators:start

これにより、http://localhost:5001/{projectId}/{region}/{functionName}/{functionPath}/ で署名検証用のCloud Functionsが立ち上がる。このURLはwebhook URLとして、後述のStripe CLIでサーバーを起動するときに指定する。
ここで、functionNameverifySignaturefunctionPathwebhook-stripe/である。
region, projectIdはご自身のFirebaseに作成したプロジェクトのリージョン及びプロジェクトIDを参照すること。

Stripe

アカウント作成

省略

CLIのインストール
$ brew install stripe/stripe-cli/stripe
ログイン
$ stripe login
# ブラウザに遷移するので、画面に従い認証を行う
CLIによるサーバー起動
$ stripe listen --forward-to http://localhost:5001/{projectId}/us-central1/verifySignature/webhook-stripe/
# 署名シークレットが表示されるため、これで index.ts 内で指定するものを更新し、再度firebase emulators:start で起動し直す

Test & Result

リクエストの形式

$ stripe trigger <EVENT_NAME>

に入れるべきものは、以下で確認可能

$ stripe trigger --help
...
Supported events:
  account.updated
  balance.available
  charge.captured
  ...

実際にstripeに何らかのリクエストを投げる

$ stripe trigger balance.available
...
functions: Beginning execution of "us-central1-verifySignature"
  {
    id: 'evt_1JGOSGFZkFlFEY6jnM5qUSGL',
    object: 'event',
    api_version: '2020-08-27',
    created: 1627047019,
    data: {
      object: {
        id: 'pi_1JGOSEFZkFlFEY6jH4vzdVHS',
        object: 'payment_intent',
        amount: 2000,
        amount_capturable: 0,
        amount_received: 2000,
        application: null,
        application_fee_amount: null,
        canceled_at: null,
        cancellation_reason: null,
        capture_method: 'automatic',
        charges: [Object],
        client_secret: 'pi_1JGOSEFZkFlFEY6jH4vzdVHS_secret_oIsYsVEmk91lO3D8RERkUU0JO',
        confirmation_method: 'automatic',
        created: 1627047018,
        currency: 'usd',
        customer: null,
        description: '(created by Stripe CLI)',
        invoice: null,
        last_payment_error: null,
        livemode: false,
        metadata: {},
        next_action: null,
        on_behalf_of: null,
        payment_method: 'pm_1JGOSEFZkFlFEY6jre53Hhu9',
        payment_method_options: [Object],
        payment_method_types: [Array],
        receipt_email: null,
        review: null,
        setup_future_usage: null,
        shipping: [Object],
        source: null,
        statement_descriptor: null,
        statement_descriptor_suffix: null,
        status: 'succeeded',
        transfer_data: null,
        transfer_group: null
      }
    },
    livemode: false,
    pending_webhooks: 3,
    request: { id: 'req_f1Qn79RYEzu0Lm', idempotency_key: null },
    type: 'payment_intent.succeeded'
  }
i  functions: Beginning execution of "us-central1-verifySignature"
i  functions: Beginning execution of "us-central1-verifySignature"
  {
    id: 'evt_1JGOSGFZkFlFEY6j57JY0AMQ',
    object: 'event',
    api_version: '2020-08-27',
    created: 1627047019,
    data: {
      object: {
        id: 'ch_1JGOSFFZkFlFEY6jjreb2LGJ',
        object: 'charge',
        amount: 2000,
        amount_captured: 2000,
        amount_refunded: 0,
        application: null,
        application_fee: null,
        application_fee_amount: null,
        balance_transaction: 'txn_1JGOSFFZkFlFEY6jXw83a3Wy',
        billing_details: [Object],
        calculated_statement_descriptor: 'Stripe',
        captured: true,
        created: 1627047019,
        currency: 'usd',
        customer: null,
        description: '(created by Stripe CLI)',
        destination: null,
        dispute: null,
        disputed: false,
        failure_code: null,
        failure_message: null,
        fraud_details: {},
        invoice: null,
        livemode: false,
        metadata: {},
        on_behalf_of: null,
        order: null,
        outcome: [Object],
        paid: true,
        payment_intent: 'pi_1JGOSEFZkFlFEY6jH4vzdVHS',
        payment_method: 'pm_1JGOSEFZkFlFEY6jre53Hhu9',
        payment_method_details: [Object],
        receipt_email: null,
        receipt_number: null,
        receipt_url: 'https://pay.stripe.com/receipts/acct_1JExhyFZkFlFEY6j/ch_1JGOSFFZkFlFEY6jjreb2LGJ/rcpt_JuCrnUA8LSppUxCxBQ58XX5CbdP0igP',
        refunded: false,
        refunds: [Object],
        review: null,
        shipping: [Object],
        source: null,
        source_transfer: null,
        statement_descriptor: null,
        statement_descriptor_suffix: null,
        status: 'succeeded',
        transfer_data: null,
        transfer_group: null
      }
    },
    livemode: false,
    pending_webhooks: 3,
    request: { id: 'req_f1Qn79RYEzu0Lm', idempotency_key: null },
    type: 'charge.succeeded'
  }
  {
    id: 'evt_1JGOSGFZkFlFEY6jXdUgwE4B',
    object: 'event',
    api_version: '2020-08-27',
    created: 1627047018,
    data: {
      object: {
        id: 'pi_1JGOSEFZkFlFEY6jH4vzdVHS',
        object: 'payment_intent',
        amount: 2000,
        amount_capturable: 0,
        amount_received: 0,
        application: null,
        application_fee_amount: null,
        canceled_at: null,
        cancellation_reason: null,
        capture_method: 'automatic',
        charges: [Object],
        client_secret: 'pi_1JGOSEFZkFlFEY6jH4vzdVHS_secret_oIsYsVEmk91lO3D8RERkUU0JO',
        confirmation_method: 'automatic',
        created: 1627047018,
        currency: 'usd',
        customer: null,
        description: '(created by Stripe CLI)',
        invoice: null,
        last_payment_error: null,
        livemode: false,
        metadata: {},
        next_action: null,
        on_behalf_of: null,
        payment_method: null,
        payment_method_options: [Object],
        payment_method_types: [Array],
        receipt_email: null,
        review: null,
        setup_future_usage: null,
        shipping: [Object],
        source: null,
        statement_descriptor: null,
        statement_descriptor_suffix: null,
        status: 'requires_payment_method',
        transfer_data: null,
        transfer_group: null
      }
    },
    livemode: false,
    pending_webhooks: 3,
    request: { id: 'req_f1Qn79RYEzu0Lm', idempotency_key: null },
    type: 'payment_intent.created'
  }
i  functions: Finished "us-central1-verifySignature" in ~1s
i  functions: Finished "us-central1-verifySignature" in ~1s
i  functions: Finished "us-central1-verifySignature" in ~1s
i  functions: Beginning execution of "us-central1-verifySignature"
  {
    id: 'evt_1JGOSGFZkFlFEY6jPyo2vocQ',
    object: 'event',
    api_version: '2020-08-27',
    created: 1627047020,
    data: {
      object: {
        object: 'balance',
        available: [Array],
        livemode: false,
        pending: [Array]
      }
    },
    livemode: false,
    pending_webhooks: 3,
    request: { id: null, idempotency_key: null },
    type: 'balance.available'
  }

すなわち、署名検証を行なった結果、Stripeからのリクエストであることが証明された。

Couclusion

Firebase Cloud Functionsのローカルエミュレーションを用いて、Stripeの署名検証テストを行うことができた。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?