スマホ

Androidアプリ内課金のレシート検証について

スマホのアプリ内課金は、ゲームのポイントのような都度課金や、音楽聞き放題のような月額のサブスクリプション含め当たり前の機能となってきました。

スマホを使って有料のサービスを受けたことがある人なら、以下のいずれかを利用している可能性は高いのではないでしょうか。

Google(Inapp Billing)
Apple(Inapp Purchase)
Amazon(Inapp Purchase)

課金する側であればスマホを操作するだけで難しいことはないのですが、サービスを提供する側になると色々と悩ましい問題が発生します。

今回は、アプリ内課金の実装について調べる機会があったので、特に Google(Android)の仕様についてまとめておきたいと思います。

アプリ内課金の流れ

アプリ内課金の流れは Google でも Apple でも大きな違いはなく、ネイティブアプリでの決済だけであればそれほど苦労することはありません。

例えば Google だと、以下にネイティブアプリの実装で使用するフローや API についてのドキュメントが用意されています。

ネイティブアプリ単体であれば、ストア(GooglePlay Store)の商品に対して「決済」や「消費」をすることは Android 開発者なら朝飯前なのかもしれませんね。

よって今回はネイティブアプリではなく、ネイティブで決済した後のレシート情報をサーバサイドで検証する部分にフォーカスしてみます。

サーバサイドでレシート検証する必要性

ネイティブで決済した際、ストアから決済レシートが発行されます。

多くのサービスでは、レシートの内容(例えば購入した商品ID)を参考にポイントなど何かしらのインセンティブをユーザに付与すると思います。

ただしネイティブアプリ側でレシートの内容を鵜呑みにしてしまうと、レシートの改竄などセキュリティ面のリスクがあるため、サーバサイドから Google の Purchase API を使ってレシート検証するのが一般的になっているでしょうか。

フローイメージ

アプリで決済する
レシートと署名が発行される
レシートと署名をサーバサイドへ送信
サーバサイドで署名の整合性を確認
サーバサイドでレシートの整合性を確認
サーバサイドから Purchase API でレシート確認
API で返されたレシートの整合性を確認
ユーザへポイントなどを付与
アプリで決済ステータスを消費にする

ただし、サーバサイドでレシート検証を実装する際に気をつけたいポイントが大きく 3 つあります。

アプリから送信された情報のチェック
Purchase API とのやり取り
重複課金を防ぐ仕組み

初めてこの手の実装を行う際は未知な部分が多くて、ネイティブアプリと結合するまで不安です。

そこでハマりそうなポイントを 1 つずつ見ていきたいと思います。

アプリから送信された情報のチェック

ストアの商品を購入すると、ネイティブアプリ側でレシートと署名の情報が取得できます。

レシート情報は JSON 文字列のデータですが、署名は Base64 エンコードされた長い文字列になります。

サーバサイドではまず、この署名がアプリから正しく送信されてきたものかどうかをチェックします。必要な情報は以下の通り。

レシートの情報(JSON)
署名(文字列)
アプリの公開鍵

「アプリの公開鍵」は GooglePlay Console 上でアプリごとに管理されているものになります。

要は、これらの情報を使ってレシートの SHA1 ハッシュ値と公開鍵で復号化した署名を照合するわけです。

これを PHP で書くと以下のようなイメージ。

せっかく Android が絡むので、サーバサイド側の署名チェックも Kotlin で書いてみましょうか。

必要な部分だけ抜粋してエラー処理は省きます。

これで署名が正しいことが確認できれば次のステップです。

Purchase API とのやり取り

次に、レシートの情報を Google の Purchase API から取得して、アプリから送られてきたレシートとの整合性を確認します。

Purchase API のエンドポイントとリクエスト・レスポンスの仕様は以下の通りなので、まずは API を叩くのに必要な情報を揃えておきます。

エンドポイントの URL パスには以下の 3 つの情報を埋め込みます。全てレシート情報(JSON)に記載されている値です。

https://www.googleapis.com/androidpublisher/v3/applications/[packageName]/purchases/products/[productId]/tokens/[token]

packageName: アプリのパッケージ名
productId: 商品ID
token: トークン

ただし、この API を実行するには、Google のクライアント認証でアクセストークンを取得しておく必要があります。

以下のドキュメントを参考にしてもらうとわかりますが、OAuth の API を叩いて取得します。

Authorization

Scope: https://www.googleapis.com/auth/androidpublisher

必要に応じてリフレッシュトークンも使用しますが、面倒であればライブラリに任せてしまうのも手ですね。

Gradle を使っている場合は、build.gradle の dependencies に以下の定義を追加します。

compile ではなく implementation を使いましょうって話ですね。

Gradleのcompileは非推奨なのでapiかimplementationを使う夏頃から携わっているプロジェクトで初めて SpringBoot を利用しています。 今回の開発言語は Kotlin でしたが、これまで...

プログラム的にはこれでいいのですが、この API を利用するには Google の「サービスアカウント」を作成して、アカウント作成時に発行される認証ファイル(JSONまたはp12)を取得しておく必要があります。

GooglePlay Console 上のネイティブアプリ一覧画面にアクセスすると、左メニューに「設定」がありますので、ここからサービスアカウントの発行や API の利用許可をすることができます。

ここまで準備できたら、Google のクライアント認証で取得したアクセストークンを Purchase API の URL にリクエストパラメータとして付与します。

https://www.googleapis.com/androidpublisher/v3/applications/[packageName]/purchases/products/[productId]/tokens/[token]?access_token=[accessToken]

各パラメータに問題がある場合は 400、認証情報に問題がある場合は 401 など、Google からレスポンスのステータスコードとエラーが返されますが、このあたりの詳細な仕様はどこにも記載されていないのでもどかしいところです。

API のリクエストに問題がなければ、レシート情報が返ってきますので必要事項を確認して整合性チェックを完了します。

テスト購入判定
アプリから送信されたレシートとの照合
消費されていないかの確認

レスポンスの purchaseType の有無で、テスト購入かどうかも判断できるので、この API のチェックは必ずやっておきたいところです。

{
“kind”: “androidpublisher#productPurchase”,
“purchaseTimeMillis”: long,
“purchaseState”: integer,
“consumptionState”: integer,
“developerPayload”: string,
“orderId”: string,
“purchaseType”: integer
}

重複課金を防ぐ

重複課金をどのように制御するかは、プロジェクトの実装や運用方法によって方針が異なると思います。

私は、テスト購入の際には「注文ID(OrderId)」がレシート情報に入ってこないという仕様を読んでいたので、purchaseToken を活用して一意性を給う方向で検討していたのですが、テストアプリで試したところ OrderId は含まれていました。

よってアプリ単位で、同じ OrderId のレシートを重複して許容しなければ良さそうです。
(アプリに関係なく OrderId はユニークになっていると思いますが)

しかし Google のドキュメントには、「テスト購入(サンドボックス)では OrderId が情報として入ってこないので注意しろ」と書いてあるのですが、実際にはテスト購入時にも OrderId は入ってきているのですよね。

これは純粋に、Google のドキュメントが古い可能性がありますがどうなんでしょうか。英語版ドキュメントを見た方が無難かも?

注: テスト購入には orderId フィールドがありません。テストトランザクションをトラックするには、purchaseToken フィールドを使用してください。

アプリ内課金の管理 – 注文番号を使う

その他の注意事項

その他、いくつかハマった点を備忘録として残しておきます。

Developer Payload

レシート情報の整合性を確認する際、これまでは「Developer Payload」の項目をレシートに含める方法が多くとられていたようです。

例えば、そのプロジェクトでしかわかりえないような値(ユーザIDとか)をネイティブアプリでレシート情報に追加し、それをサーバサイド側で照合するという方法です。

私も最初、この手法も取り入れようと思っていたのですが、「クックパッド開発者ブログ」でも書かれている通り「Developer Payload」の項目を利用する方法は推奨されなくなっています。

よってここまで書いてきた通り、サーバサイドから Purchase API でレシート情報を確認するのがベターです。

あとは、個人情報やセキュアな情報が残らない程度に、決済に必要な情報をログに残しておくといったところでしょうか。

Purchase APIで401エラー

検証の過程で、Google の Purchase API を叩く際に 401 エラーに遭遇することがありました。

原因はアクセストークンの権限不足と思われます。

アカウントサービスの権限設定のときに触れましたが、この原因で引っ掛かる人の多くは GooglePlay Console 上でアカウントサービスの API 利用承認していないパターンになります。

利用許可をした記憶がない人は、GooglePlay Console の設定メニューを再度確認してみてください。

ただし、GooglePlay Console 上で承認したにも関わらず 401 エラーが続く場合は、権限の反映に時間がかかっている可能性があります。

実際に私がそうだったのですが、API の利用許可ボタン押下後、数時間経過しても 401 で弾かれ続けました。

その際、以下の情報に行き当たったのですが、そこのコメントにも書かれていた通り、割り切って次の日の朝まで待ってみたら疎通確認ができました。

Google 翻訳で訳したので、わかりにくかったらすみません。

I think that the cause of the problem is that it is necessary to wait several days before all methods of api starts working correctly. But I not found this in docs.

私は問題の原因はapiのすべての方法が正しく機能し始める前に数日待つ必要があることであると思います。しかし、私はこれをドキュメントで見つけませんでした。

Yeah, you’re right. Just had to wait a couple days.

ええ、その通りです。ほんの数日待たなければなりませんでした。

正式な情報がないのでこれが正しいかどうかは何とも言えませんが、とにかく慣れていないと設定周りでは疲弊します・・・。
(だから、この備忘録を書いているわけですが)

検証を進めていく上で、新たな問題や訂正が必要な項目が出てきたら追記していきます。