WebPay Receiptを例にしたExtendアプリケーションの作り方

他のWebPayユーザに、そのユーザのデータを使ったサービスを提供できる、WebPay Extendを先日リリースしました

WebPay ExtendはOAuth2に則ったオーソドックスな仕組みで提供されており、扱うオブジェクトも通常WebPayを利用する場合と同一です。 とはいえ、Extendアプリケーションの着想を得にくかったり、細部でどう処理するのが正しいのかわかりにくい箇所もあるでしょう。

そこで、WebPay Extendをすぐに体験でき、実際のサービスの運用にも便利に使えるWebPay Receiptのソースコードを公開しています。 この記事では、WebPay Receiptの機能を紹介し、内部の実装を簡単に説明しながらExtendアプリケーション作成の足掛かりを提供します。

WebPay Receiptを使ってみる

WebPay Receiptは、顧客オブジェクトに課金が作成されたとき、顧客のメールアドレスに領収書を自動で送るサービスです。 WebPayのアカウントがあれば簡単に試せます。ちょっとやってみましょう。

まずはWebPay Receiptにアクセスします。

images

Extendアプリケーションを初めて使うときは、まずテスト環境のみで利用してみましょう。 WebPay Receiptではテスト環境で試した後、商用環境にも適用するか選べます。 「テスト環境で利用を開始する」を押すと、WebPayのサイトに飛び、「アクセスの確認」画面が表示されます。 権限の内容と利用規約に同意できる場合は、許可してください。

WebPay Receiptのページに戻り、送信履歴が表示されますが、まだ何も無いようです。 WebPay Receiptはメールアドレスが登録されている顧客オブジェクトに、新たな課金が作成されたときに動作します。

images

まずは、領収書のメールの文面を作成しましょう。中央の「送信内容を設定する」もしくは、右上の「メール設定」から情報を設定します。 名称とサイトのURLは必須項目です。これらがないと、受信した人がどの支払いのメールかわからなくなってしまいます。

images

それでは実際に課金を作成して、その領収書の送信を確認してみます。 WebPayのテスト環境のダッシュボードを開き、顧客の一覧から新しい顧客を追加します。 メールアドレスには確認できるように自分のメールアドレスを、カード番号はテスト用クレジットカードのものを使いましょう。 顧客が作成できたら、中程の「課金履歴」の右側、「新規の課金を作成」から、金額を入力して課金を実施します。

数分待つと、メールボックスにメールが届くはずです。 WebPay Receipt上の送信履歴にも該当のメールが表示されます。

images

認可したExtendアプリケーションの一覧はユーザ設定の下部で見ることができます。 いま見にいくと、WebPay Receiptが表示されているでしょう。

利用していないExtendアプリケーションを認可したままにしないようにしましょう。 意図せず情報を読み取られてしまう恐れがあります。 もしWebPay Receiptが期待はずれで、もう使わないなと思ったら、ここで削除しておきましょう。

Extendアプリケーションの作り方

さて、Extendアプリケーションの作り方について解説していきます。

まずはWebPay Extendのドキュメントをよく読み、内容を把握してください。

その上でExtendアプリケーションを登録し、READMEに従って、 cloneしたWebPay ReceiptのRailsアプリケーションを前述したひと通りの動作が行われるところまで試してください。

もし分からないことがあったら、フォーラムにてご質問ください。

では実際にWebPay ReceiptがどうWebPayとコミュニケーションをとり、領収書を送付する機能を提供しているか見ていきましょう。 もう一度WebPay Receiptの使い方を確認します。

  1. テスト環境へのアクセスを認可する
  2. メールテンプレートを作成する
  3. メールアドレスをもった顧客を作る
  4. その顧客に対して課金を作る
  5. メールが届く

WebPay ReceiptとWebPayのやりとりを見てみると、1でWebPay Extendの認可機能を利用してユーザ情報とユーザのWebPayアカウントへのアクセス権を得ます。 4でWebPayがアプリケーションWebhookをWebPay Receiptのサーバに送信します。この中身を確認してメール文面を作成し、ユーザに送信します。 具体的にGitHubにあるソースコードを参照しながら、これらの実装を説明します。

Landing

app/views/dashboard/_landing.html.haml

landingページでは「テスト環境で利用を開始する」と表示しています。 WebPay Extendのドキュメントにあるように、Extendアプリケーションは最初にテスト環境で試すことができる動線を用意しなければいけません。 WebPay Receiptはユーザが必ずテスト環境で試すよう、最初はリンクしか見えなくなっています。

UsersController

app/controllers/users_controller.rb

それでは、ボタンを押したつもりで認可のフローを見てみましょう。

1
2
3
4
5
6
7
8
9
def sign_in
  oauth_session_id = SecureRandom.hex(32)
  session[:oauth_session_id] = oauth_session_id
  livemode_scope = params[:livemode].to_s == 'true' ? 'live' : 'test'
  if (current_user && livemode_scope == 'test') || (current_user && current_user.live_activated? && livemode_scope == 'live')
    return redirect_to(root_path, notice: 'すでに認可しています')
  end
  redirect_to client.auth_code.authorize_url(redirect_uri: redirect_uri, scope: 'read ' + livemode_scope, state: oauth_session_id)
end

OAuthのstateパラメータで、CSRF対策をしています。

  • ランダムな文字列を作成して、ブラウザのセッションと関連づけ、
  • ブラウザをWebPayの認可画面に転送するときにstateにこの文字列を渡し、
  • コールバックのstateをセッションの値と照合する

ことで、コールバックされてきたブラウザが、認可を要求したブラウザと同一であることを確かめ、第三者による乗っ取りを阻止します。 Extendアプリケーション作成時はかならずstateを利用してください。

最後の行でoauth2 gemOAuth2::Clientを使って、認可画面にリダイレクトしています。 このアプリケーションでは読み取りしか行わないので、常にreadスコープを指定します。

ユーザがWebPayの認可画面でアクセス権限を認めると、callbackにリダイレクトされてきます。 config/routes.rb/user/callbackへのGETリクエストを受け付けるよう指定し、Extendアプリケーションのリダイレクト先に、このURLを指定しています。

1
2
3
4
5
6
7
8
9
10
def callback
  code = params[:code]
  if session[:oauth_session_id].nil? || params[:state] != session[:oauth_session_id]
    return redirect_to root_path, flash: { error: '再度ログインしてください' }
  end
  token = client.auth_code.get_token(code, redirect_uri: redirect_uri)
  user = User.update_token(token)
  session[:user_id] = user.id
  redirect_to root_path
end

上述のように、まずstateの値を照合し、失敗したら再度ログインを促します。 リダイレクトで渡されたcodeを使ってget_token()を呼び出し、トークンをユーザモデルに保存します。 セッションのuser_idに返されたユーザのIDを設定することでログイン完了です。

User model

app/models/user.rb

update_token()では、渡されたトークンからWebPayへのアクセス情報を取り出し、該当のユーザの情報を更新します。 未登録なら新規作成します。

1
2
3
4
5
6
7
8
def self.update_token(token)
  user = User.find_or_initialize_by(webpay_id: token['webpay_user_id'])
  %w(live_secret_token test_secret_token).each do |field|
    key = token[field.gsub('_token', '_key')]
    user[field] = key if key.present?
  end
  user.tap(&:save)
end

Extendドキュメントのトークンの説明にあるように、WebPayから発行されるトークンには、通常のOAuth2のパラメータに加え、さまざまな情報を付加しています。 これはWebPayが認証鍵で操作する環境やアクセス可能なAPIを分ける設計をとっており、Extendアプリケーションでもその使い勝手を引き継ぎたいがためです。 webpay_user_idがユーザの固有識別子なので、これをReceiptのユーザを識別するためにも使います。 4つの鍵を更新し、保存してユーザインスタンスを返します。

WebhookController

ユーザの登録を受け付けられたので、次は課金が作成され、Webhookが飛んできたときの処理です。 app/controller/webhook_controller.rbは2種類のWebhookを処理しているので、それぞれ見ていきましょう。

1
2
received_user =
  User.find_by(webpay_id: request.headers['X-Webpay-Owner-Account-Id'])

まず、X-Webpay-Owner-Account-IdリクエストヘッダからWebhookを送信したユーザのアカウントIDを取得し、ユーザをみつけます。 ユーザが見付からなかったら処理する必要はないので、no_contentを返します。 Webhookの受信側が2xx以外のステータスコードを返すと、WebPayのシステムが一定時間後に再送します。 したがってエラーのケースでも、再送されても処理できない場合はエラーステータスではなく、正常ステータスを返してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
return head :no_content unless received_user.receipt_mail_config
charge = WebPay::ChargeResponse.new(params[:data][:object])
return head :no_content unless charge.customer && !charge.shop
email = charge.retrieve_customer(received_user).try(:email)
return head :no_content if email.blank?

return head :no_content if
  ReceiptMail.find_by(webpay_charge_id: charge.id)
record = ReceiptMail.new(
  user: received_user,
  livemode: charge.livemode,
  webpay_charge_id: charge.id,
  email: email)
begin
  ReceiptMailer.receipt(received_user, charge, email).deliver
  record.sent = true
rescue Net::SMTPError
  record.sent = false
end
record.save!

charge.succeededタイプのイベントが送信された場合の処理が上述のコードです。

  • メール設定がなされていることを確認する
  • 課金に顧客が紐付いていることを確認する
  • 顧客オブジェクトをWebPayのAPIから取得
  • メールアドレスが登録されていることを確認する
  • メールが重複してしまうことがないか確認する
  • ReceiptMailインスタンスを作る
  • ReceiptMailerでメールを送信する

という流れになります。

ReceiptMailは過去のメール一覧を管理するためのモデル、ReceiptMailerは一般的なHTMLを送信するためのメーラーです。 興味のある方はGitHubで確認してください。

注意深い方は、charge.retrieve_customer()という通常のWebPay::ChargeResponseクラスにはないメソッドを利用していることに気が付いたかもしれません。 じつは、Ruby2で導入されたrefinementを使ってWebPay::ChargeResponseを拡張しています。 このメソッドの実装はapp/models/mailable_charge.rbにあります。

1
2
3
4
5
6
7
8
9
module MailableCharge
  refine WebPay::ChargeResponse do
    def retrieve_customer(user)
      @retrieve_customer ||= user.webpay_client(livemode ? :live : :test).customer.retrieve(customer)
    rescue WebPay::ErrorResponse::InvalidRequestError
      nil
    end
  end
end

このような実装にした理由として、領収書のドメインでは課金が主体となるため、顧客を課金に関連づけて扱いたかったこと、WebPayのAPIを複数箇所で呼び出すのを避ける必要があったこと、顧客の情報はWebhook受信からメール送信までのあいだ存在すれば十分であり、永続化する必要がなかったことなどが挙げられます。

1
2
3
if params[:data][:object][:object][:id] == ENV['WEBPAY_CLIENT_ID']
  received_user.destroy
end

もうひとつ、account.application.deauthorizedイベントも処理しています。 ユーザが設定画面からExtendアプリケーションの認可を取り消したとき、このイベントが送信されます。 取り消されたのがこのアプリケーションの認可であることを確認し、ユーザのデータを削除します。 認可が取り消された時は必ずデータを削除してください。

以上で、ExtendアプリケーションとWebPayがやり取りする3つの手段、OAuthの認可、Webhook、APIアクセスについて確認しました。 実装する上での注意点やノウハウが伝われば幸いです。

今後もExtendアプリケーションを通じてWebPayにあったらよい機能を提供したり、サンプルアプリケーションやブログ記事でノウハウを紹介していく予定です。 ぜひ開発者ブログをウォッチしてください。