Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: StripeのWebhookイベントをセキュアに扱う方法(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: StripeのWebhookイベントをセキュアに扱う方法(翻訳)

StripeはWebペイメントサービスでその名を広く知られたソリューションであり、私たちは支払い処理のために構築した多くのRuby on RailsアプリケーションでStripeを大規模に採用しています。これらのアプリケーションはどれも、Stripeから届くいくつかの重要なイベントに関する通知を受け取って、ワークフローの中で対応する必要があります。

Stripeはwebhookイベントを送信することで、登録されているアプリケーションに対して、さまざまなイベントの発生時に公開エンドポイントに対してHTTP POSTリクエストの形で通知を送信します。このエンドポイント(ルーティング)は一般公開されるので、その気になれば誰でも悪意を持って呼び出せます。したがって、有害なユーザーからエンドポイントを守ることが非常に重要になってきます。Stripeが送信するStripe-Signatureヘッダーはメッセージの真正性検証に利用され、これによって知らない第三者からのリクエストではなくStripeからのリクエストであることを確認します。

これはstripe_events gemを使うのが最も簡単です。

それでは実装してみましょう。とても興味深い内容になるはずです。

1. gemのインストール(Gemfile)

Gemfileに以下のgemを追加します。

gem 'stripe_event'

2.  ルーティングの追加(routes.rb)

mount StripeEvent::Engine, at: '/any-path-you-want'

ここではわかりやすくするために、以下のパスを使うことにします。

mount StripeEvent::Engine, at: '/stripe/webhook' # このURLは変更可能

3. development環境でのテスト

まずはwebhookをローカルでテストするために、サーバーをngrokで公開しましょう。ngrokで一般からアクセス可能なURLを作成できることはもう皆さんご存知ですよね。

私のngrok URLを仮にhttps://9123ab3f.ngrok.io/だとしましょう(訳注: もちろん既に無効です)。

次に必要なのは、Stripeのwebhookを受けるhttps://9123ab3f.ngrok.io/stripe/webhookのようなエンドポイントを追加することです。

それではStripeのwebhookをセットアップしましょう。

  • Stripeアカウントにログインして、Stripeのダッシュボードに移動する
  • Developers > Webhooks を開く(リンク
  • 自分のアカウントをtestモードに切り替える
  • Stripe webhookに自分のEndPointを追加し、欲しいイベントを設定する(課金の作成や返金など)
  • 設定するEndPointはngrokのURLです(https://9123ab3f.ngrok.io/stripe/webhookなど)

liveモードのときには、自分のWebサイトのURLホストをEndPointに追加する必要があります。

webhookを作成したら、そのwebhookの署名用の秘密鍵をメモしておきます。これは後でコードに追加する必要があります。

4. credentialの追加(config/initializers/stripe.rb)

Stripe.api_key             = ENV['STRIPE_SECRET_KEY']
StripeEvent.signing_secret = ENV['STRIPE_SIGNING_SECRET']

StripeEvent.configure do |events|
 events.subscribe 'charge.dispute.created', Stripe::EventHandler.new
end

ここではStripeイベントをひとつ設定します。このcharge.dispute.createdStripe::EventHandlerは、webhookが呼び出された後のアフターエフェクトを扱うために作成したサービスです。

Stripeの支払いで何らかの課金が発生すると、Stripe::EventHandlerクラスを呼び出します。

StripeEvent.configureブロックでイベントを複数定義することもできます。詳しくは以下の記事をどうぞ。

参考: Handling BigCommerce Webhooks in Ruby on Rails application - BoTree Technologies

課金に関連する以下のようなイベントは、すべてcharge.disputeで扱えます。

  1. Charge Created(課金の作成)
  2. Charge Updated(課金の更新)
  3. Charge Closed(課金のクローズ)
  4. Charge Funds Reinstated(課金額の再投入)
  5. Charge Funds Withdrawn(課金額の回収)
events.subscribe 'charge.dispute.', Stripe::EventHandler.new

上のコードは、いずれかのcharge.disputeイベントが発火したときに、エンドポイントにリダイレクトしてStripe::EventHandlerクラスを呼び出すようStripに指示します。

5. webhookリクエストを扱うサービス

services/stripeフォルダの下にevent_handler.rbというサービスをひとつ作成します(app/services/stripe/event_handler.rb)。

# Stripeモジュール
module Stripe
 # stripeのメインクラスEventHandler
 class EventHandler
   def call(event)
     method = 'handle_' + event.type.tr('.', '_')
     send method, event
   rescue JSON::ParserError => e
     render json: { status: 400, error: 'Invalid payload' }
     Raven.capture_exception(e)
   rescue Stripe::SignatureVerificationError => e
     render json: { status: 400, error: 'Invalid signature' }
     Raven.capture_exception(e)
   end
 end

 def handle_charge_dispute_created(event)
   # your code goes here
 end
end

# method = 'handle_' + event.type.tr('.', '_')

# event.type
# これはイベント名を取れる。この記事の場合は「charge.dispute.created」を返して
# ‘.’ を ‘_’に置き換えることで‘charge_dispute_created’にし、そこに'handle_’をくっつける
# 最終的にhandle_charge_dispute_createdというメソッド名になる

# send method, event

# これはメソッドの変数の値に基づいてメソッドを呼び出す。この記事の場合は
# ‘handle_charge_dispute_created’を呼び出す
# 他のイベントについても同様にイベントに基づいてこのメソッドをサービス内に定義し
# 自分のサービスに置く必要がある

Stripeのイベントに基づいてこのようにサービスを定義するときに、このコンセプトを継承するとコードをシンプルに書けます。

以下の2つのStripeイベントを扱わなければならなくなったとします。

  • Charge dispute(課金の異議申し立て)
  • Charge refund(課金の返金)

この場合、以下のようなサービスを定義してコードを切り離せます。

メインのservice(EventHandler)でリクエストを扱います。

# app/services/stripe/event_handler.rb
# Stripe module
module Stripe
  # stripe main class EventHandler
  class EventHandler
    def call(event)
      method = 'handle_' + event.type.tr('.', '_')
      send method, event
    rescue JSON::ParserError => e
      render json: { status: 400, error: 'Invalid payload' }
      Raven.capture_exception(e)
    rescue Stripe::SignatureVerificationError => e
      render json: { status: 400, error: 'Invalid signature' }
      Raven.capture_exception(e)
    end
  end
end

以下のサービスは、リクエストに応じて課金の異議申し立てイベントを扱います。

# app/services/stripe/dispute_event_handler.rb
# Stripe event handler for handling webhook
module Stripe
  # This will inherite the eventHandler main class
  class DisputeEventHandler < EventHandler
    def handle_charge_dispute_created(event)
      # your code goes here
    end
  end
end

以下のサービスは、リクエストに応じて課金の返金イベントを扱います。

# app/services/stripe/refund_event_handler.rb
# Stripe event handler for handling webhook
module Stripe
  # This will inherite the eventHandler main class
  class RefundEventHandler < EventHandler
    def handle_charge_refund_created(event)
      # your code goes here
    end
  end
end

目が離せませんね!皆さまもぜひ、自分のStripe webhookをセキュアにすることを検討しましょう。

お読みいただいた皆さまに感謝いたします。


25人を超えるRuby on Railsチームを擁する私たちBoTree Technologiesは、エンタープライズアプリケーションの構築をお引き受けいたします。

弊社では、RPA、AI、Python、Django、JavaScript、ReactJSにも特化しています。

This blog is originally published at BoTree Technologies

関連記事

Stripe決済を自社サービスに導入してわかった5つの利点と2つの惜しい点


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。