💳

Stripe を導入したときに AWS 関連の設定で嵌ったこと

2023/12/17に公開

https://zenn.dev/k0kishima/books/f07cffba6e0fab

経緯

先日、決済機能で Stripe を利用しているサービスの開発に関わりました。
インフラには AWS が採用されていましたが、クラウド上の設定が原因で Stripe とうまく疎通ができなかったりと結構辛いトラブルがありました。

Stripe を使ってるかどうかに関わらず、HTTPヘッダーの制限であったり環境変数の設定であったり WEB 開発であればまた似たようなことがないとも限らないので、今後の教訓として細かい経緯を残しておくことにしました。

念の為誤解がないように記載しておくと、Stripe や AWS のAPIやドキュメンテーションに関して何か悪いことは一切ありませんでした。
当方は決済関連の実装は過去に行ったことはありますが、 Stripe を利用したのは今回が初でした。
Stripe は過去に利用した他の決済サービスよりも DX(Developer eXperience) が非常に高く驚きました。
AWS に関しては言わずもがなですがシェアが高いため情報も相対的に豊富であり、 IaCツール (Terraform) との統合もスムーズにできるので作業効率は高かったです。

発生した問題

  • Stripe の API へリクエストすると 'Invalid character in header content ["Authorization"]' というエラー
  • Stripe が こちらのドメインにある Webhook をリクエストすると、署名検証に利用する 'stripe-signature' ヘッダーが存在しない

以下、各々見ていきます。

Stripe の API へリクエストすると 'Invalid character in header content ["Authorization"]' というエラー

原因は上記から自明です。

まず、Stripe のクライアントは以下のように生成しています。

import Stripe from 'stripe';

export class StripeClientFactory {
  private static instance: Stripe;

  static getInstance(): Stripe {
    if (!this.instance) {
      const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
      if (!stripeSecretKey) {
        throw new Error('STRIPE_SECRET_KEY env is not set');
      }
      const apiVersion = '2022-11-15';
      this.instance = new Stripe(stripeSecretKey, { apiVersion });
    }

    return this.instance;
  }
}

認証系のヘッダーの設定などは上記のAPIクライアントのライブラリが全部隠蔽してるのでこちらでは特に気にしてませんでした。
実際、ローカル開発環境では正常に認証ができていたので表題のようなエラーが出るのは不可解でした。

問題が発生していたのはステージング環境でしたが、環境変数を確認してみても特に間違った値が入っているようには見えませんでした(後述するがこれが事実誤認だった)。

Stripe には開発者用の Discord があるのですが同じ境遇に置かれていた人を発見しました。

https://discord.com/channels/841573134531821608/1036742011031650334/1036793248158584882

この人のケースだと、シークレットをベタ書きしたら認証は成功しているが、環境変数を利用すると失敗するということでした。

Discord 上でされていたアドバイスは、値が正しく設定されているように見えても不可視文字が紛れ込んでいる可能性もあるから、ログを出すなら encodeURIComponent を使ってみたらどうかということでした。

https://stackoverflow.com/questions/71803487/node-js-is-there-a-way-to-log-invisible-characters-in-a-string/71803539#71803539

こちらでもステージングであることシークレットをすぐに更新して現在のものは無効にするという条件でログを出してみましたが、“sk_test_51....MPz6c%0A” のように末尾に改行が入っていました。

設定したのは他の開発者だったのでどのような経緯で改行が入ったかはまだ聞いていません。

暫定対応で this.instance = new Stripe(stripeSecretKey, { apiVersion });stripeSecretKeystripeSecretKey.trim() にしてみたら収拾しました。

Stripe が こちらのドメインにある Webhook をリクエストすると、署名検証に利用する 'stripe-signature' ヘッダーが存在しない

署名検証している処理自体は公式ドキュメントでのサンプルコードとほぼ同じです。

https://stripe.com/docs/identity/handle-verification-outcomes?locale=ja-JP#create-webhook

const sig = req.headers['stripe-signature'];
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);

要するに上記のようなコードになっているのですが、何度確認しても sigundefined になり、ヘッダーに stripe-signature というフィールド自体が存在していないようでした。

ちなみに同じ境遇の人は過去にも存在していて issue が起票されていました。

https://github.com/stripe/stripe-dotnet/issues/1880

ただ、ローカルホストへの転送を利用して確認した際は実際に stripe-signature ヘッダーがありましたし、Stripe のドキュメントにもこれを省くようなケースに関する記載自体が存在しません。

なので、何かしらインフラの設定が悪さをしてるんだろうとは思っていました。

今回は CDN に Cloud Front を利用していたのですが、これにアタッチされていたポリシーがヘッダーのホワイトリスティングをしていて、そこで stripe-signature がフィルタリングされてました。
したがって、 Webhook に到達する前に stripe-signature ヘッダーが消えてしまい、検証に常に失敗するという状態になっていました。

stripe-signature を転送するように設定することで解消しました。

これも設定は自分が担当していなかったので気付くのが遅れました。

まとめ

  • 目視で同値に見えても違うと言われたら不可視文字の存在をチェック
  • Stripe は開発ツールが充実していて、ローカル開発環境でもかなりの精度で E2E のインタラクションをエミュレートできるので、開発環境で動いてるのにパブリッククラウド上で動かないならインフラ設定の方を疑った方が良さそう

Discussion