【Stripe】制限付きのキーでAPIキーをセキュアに運用する

こんにちは、サイオステクノロジーの佐藤 陽です。
今日はStripeのAPIキーのセキュリティに関するお話です。

  • StripeのAPIキーをセキュアに運用したい

という方は是非、最後までご覧ください。

はじめに

StripeのAPIを実行するにあたっては、APIキーが必要です。
これは直接エンドポイントを実行する場合も、SDK経由で実行する場合も同様です。

このAPIキーに関してはDashboard上から閲覧可能で、sk_から始まる文字列です。

このシークレットキーがあれば、Stripeの全APIを実行することができます。

もちろん漏洩しないことが大前提ですが
お金を扱うサービスという事もあり、漏洩した場合を考えるとぞっとしますね。

今回は、万が一の場合にも被害を最小限に備えるため、APIが操作可能なスコープを制限する方法をご紹介します。

制限キーの作成

StripeにはAPIの制限付きキーという機能が提供されています。

早速作成してみます。

すると、Stripeの各オブジェクトに対して

  • 権限無し
  • 読み取り
  • 書き込み

の3点が設定できることが分かります。
ちなみに「書き込み」の権限については、「読み取り」権限も含まれます。

これを細かく設定することで、必要最低限の機能をアプリケーションに与えることができます。

ではこの機能を使っていくつか検証してみたいと思います。

検証1 : 権限無しのオブジェクトに対するAPIコール

今回は、以下の設定としてキーを作成します。

  • 「Customer」「Subscriptions」のオブジェクトに対して「書き込み」を設定
  • その他のオブジェクトに対しては全て「なし」 

キーはrk_から始まる文字列です。

このキーを使って、CustomerとSubscriptionを作成してみます。
(※ProductおよびPriceに関しては事前に作成してあるものとします。)

Customer作成

STRIPE_SECRET_KEY="rk_******"

curl -L 'https://api.stripe.com/v1/customers' \
-H "Authorization: Bearer ${STRIPE_SECRET_KEY}" \
-d 'name=SIOS Taro'

Subscription作成

(※今回は、0円のPriceに基づいてSubscriptionを作成しています。
有償のものを利用する場合は、支払い方法をCustomerに割り当ててください。)

STRIPE_SECRET_KEY="rk_******"

curl -L 'https://api.stripe.com/v1/subscriptions' \
-H "Authorization: Bearer ${STRIPE_SECRET_KEY}" \
-d 'customer=cus_OhTQ5O4clMjRJC' \
-d 'items[0][price]=price_1NryeRE7qHO17eJUJjAlaPcK'

実行すると、問題なく行えるかと思います。

では次に権限が無いAPIの実行を行います

今回はListInvoiceのAPIを利用し、この顧客の持つInvoiceの一覧を取得してみます。
Invoicesに対する権限は「なし」に設定しているため、APIの実行は失敗が想定されます。

STRIPE_SECRET_KEY="rk_******"

curl -L -X GET 'https://api.stripe.com/v1/invoices?limit=3' \
-H "Authorization: Bearer ${STRIPE_SECRET_KEY}" \
-d 'customer=cus_OhTQ5O4clMjRJC'

すると、以下のレスポンスが得られました。

Status Code: 403Forbidden
{
    "error": {
        "message": "The provided key 'rk_test_*********************************************************************************************5ptNE3' does not have the required permissions for this endpoint on account 'acct_1NkeWFE7qHO17eJU'. Having the 'rak_credit_note_read' permission would allow this request to continue.",
        "request_log_url": "https://dashboard.stripe.com/test/logs/req_JyrZZynhhcIF6H?t=1695609445",
        "type": "invalid_request_error"
    }
}

想定通り、Invoiceの一覧を取得することができませんでした。
親切に何のPermissionが不足しているかまで書いていてくれますね。

 Having the ‘rak_credit_note_read’ permission would allow this request to continue

せっかくなのでエラーメッセージに従い、Credit notesに対して読み取り権限を付与して再度実行します。
(※Invoicesの権限は「なし」のまま)

するとAPIが正しく実行され、InvoiceのListが取得できました。
…取得できたのはいいんですが、Invoiceの権限が「なし」のままなのに取得できてしまっていますね。

検証のため、次は

Credit notes なし
Invoices 読み取り

としてAPIを実行してみます。

こちらも成功し、Listの内容を取得できました。

考察

そもそもCredit notesとは何なのかというと、インボイスの内容を修正する時に利用するObjectです。
つまり、Credit notesの修正を行う権限があるということは、Invoiceの閲覧権限を持っていると判断されるかもしれません。

なので

Invoiceの閲覧権限 ⊂ CreditNotesの書き込み権限

というのが成り立ちそうです。
ただ、StripeのDashboard的にはこれらの権限が並列に並んでいるので少し分かりづらいですね。

エラーメッセージの内容を鵜呑みにするのではなく、
必要最低限の権限を与えるにはどうすればいいかを自分で判断する必要がありそうです。

検証2 : Expanding機能と制限の関係性

もう一つ気になったことを検証してみます。
それが、Expanding機能との関係性です。

Expanding機能は、Responseの内容を拡張する機能です。

例えば

SubscriptionのObjectを取得した際に、レスポンスには関連するProductのIDが含まれます。
そのProductに関する詳細を取得するには、GetProductを実行する必要があり

  1. GetSubscriptionをコール
  2. (1.)で取得したProductIdを元にGetProductをコール

といったように、合計2回のコールが必要となります。

この時、Expanding機能を使うことで、一度のAPI実行でSubscriptionとProductの両方の情報を取得することができます。

  1. ProductをExpandingに設定し、GetSubscriptionをコール

ただし、今回はProductに関する読み取り権限を持っていません。
ただAPIとしてはGetSubscriptionを利用しており、APIの対象としてはSubscriptionです。

こういった場合、APIの挙動はどうなるのでしょうか?
早速検証してみましょう。

まずはExpanding無しで、先ほど作成したSubscriptionに対してGetSubscriptionを実行してみます。

STRIPE_SECRET_KEY="rk_******"

curl -L 'https://api.stripe.com/v1/subscriptions/{Subscription ID}' \
-H "Authorization: Bearer ${STRIPE_SECRET_KEY}" \

こちらは問題なく取得できると思います。

次に、ProductのExpandingを利用したGetSubscriptionを実行してみます。
Productの階層としては、先ほどのGetSubscriptionのレスポンス(抜粋)を見ると

    "ended_at": null,
    "items": {
        "object": "list",
        "data": [
            {
                "id": "si_OhTXaXvBoBGxIS",
                "object": "subscription_item",
                "billing_thresholds": null,
                "created": 1695609125,
                "metadata": {},
                "plan": {
                    "id": "price_1NryeRE7qHO17eJUJjAlaPcK",
                    "object": "plan",
                    "active": true,
                    "aggregate_usage": null,
                    "amount": 0,
                    "amount_decimal": "0",
                    "billing_scheme": "per_unit",
                    "created": 1695109639,
                    "currency": "jpy",
                    "interval": "month",
                    "interval_count": 1,
                    "livemode": false,
                    "metadata": {},
                    "nickname": null,
                    "product": "prod_OfJGQfsiT6ZNgJ",  //←ココ!!!
                    "tiers_mode": null,
                    "transform_usage": null,
items.data.plan.product

であることが確認できます。
そのため、Expandingにはitems.data.plan.productを指定します。

STRIPE_SECRET_KEY="rk_******"

curl -L 'https://api.stripe.com/v1/subscriptions/{Subscription ID}' \
-H "Authorization: Bearer ${STRIPE_SECRET_KEY}" \
-d 'expand[]=items.data.plan.product'

すると、以下のレスポンスが得られました。

{
    "error": {
        "message": "The provided key 'rk_test_*********************************************************************************************5ptNE3' does not have the required permissions for this endpoint on account 'acct_1NkeWFE7qHO17eJU'. Having the 'rak_product_read' permission would allow this request to continue.",
        "request_log_url": "https://dashboard.stripe.com/test/logs/req_jAW2FkVfJnDczg?t=1695611089",
        "type": "invalid_request_error"
    }
}

失敗しました。

考察

Expandingで設定したオブジェクトも制限の対象となっているようです。

なお、対象のObjectの内容が取得できないだけでなく、APIコール自体が失敗してしまうようですね。
このあたり、権限の設定や実装面での注意が必要ですね。

まとめ

今回は、Stripeの制限付きキーの機能を使ってみました。
セキュリティの観点から、APIキーの権限を制限することは非常に重要です。

ただ権限レベルにも包括関係があるようで、意図しない操作が行われてしまう可能性もありそうです。
またしっかり権限のスコープを理解していないと、APIコールの失敗にも繋がってしまうことも分かりました。

オブジェクトの関係性などをしっかり理解したうえで、権限を設定する必要がありそうですね!

あとはそもそもAPIキーが漏洩しないよう、適切な管理を行うことが大前提ですね。
ではまた!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です