Ruby（Rails）でAndroidアプリ内課金のレシートを検証するコード例

# 前提
Androidアプリでの商品の購入をサーバ側でも検証する必要があった。
ので、アプリから送信された[Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase)の情報をRubyでDeveloper APIに問い合わせ、購入を確認するコードを紹介します。
以下はRailsでの例ですが、特にRails固有の機能は使っていないので他のRuby環境でも動くはず。

# 検証
先に全体のコードを貼る。
[検証用トークン](https://developer.android.com/reference/com/android/billingclient/api/Purchase.html#getpurchasetoken)と[プロダクトID](https://developer.android.com/reference/com/android/billingclient/api/Purchase.html#getSku())を放り込み、[購入情報](https://developers.google.com/android-publisher/api-ref/purchases/products#resource)が返ってくれば検証成功というもの。

```ruby
require 'jwt'
require 'net/http'
require 'openssl'

class ReceiptValidator
  def validate_transaction(purchase_token, product_id)
    order_id = nil
    purchase_data = nil
    # [1], [2]
    access_token = issue_request_token
    # [3]
    base = "https://www.googleapis.com/androidpublisher/v3/applications/"
    url = base +  "パッケージネーム/purchases/products/#{product_id}/tokens/#{purchase_token}"
    res_string, status_code = get_request(url, access_token)

    if status_code >= 200 && status_code < 300
      response = JSON.parse(res_string)
      # [4]
      if response && response['orderId'].present? && response['purchaseState'] == 0 # 0 => Purchased
        purchase_data = res_string
        order_id = response['orderId']
      end
      return [order_id, purchase_data]
    else
      Rails.logger.error('Could not fetch PlayStore receipt data')
      Rails.logger.error("status code: #{status_code}")
      Rails.logger.error("body: #{res_string}")
      return [nil, nil]
    end
  end

  private

  def issue_request_token # [2]
    url = 'https://oauth2.googleapis.com/token'
    header = {
      'Content-Type' => 'application/x-www-form-urlencoded'
    }
    params = {
      assertion: service_auth_jwt, # [1]で作ったJWT
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'
    }
    body, status_code = post_request(url, header, params)
    if status_code >= 200 && status_code < 300
      res = JSON.parse(body)
      res['access_token']
    else
      Rails.logger.error('Could not fetch Google account access token')
      Rails.logger.error("status code: #{status_code}")
      Rails.logger.error("body: #{body}")
      nil
    end
  end

  def service_auth_jwt # [1]
    payload = {
      iss: Rails.application.credentials.google_service_account_client_email,
      scope: 'https://www.googleapis.com/auth/androidpublisher',
      aud: 'https://oauth2.googleapis.com/token',
      exp: Time.now.to_i + 3600,
      iat: Time.now.to_i
    }
    rsa_private = OpenSSL::PKey::RSA.new(Rails.application.credentials.google_service_account_private_key)
    JWT.encode(payload, rsa_private, 'RS256')
  end

  def get_request(url, token = nil)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'

    req = Net::HTTP::Get.new(uri.path)
    req['Authorization'] = "Bearer #{token}" if token
    response = http.request(req)

    [response.body, response.code.to_i]
  end

  def post_request(url, headers, params)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'

    req = Net::HTTP::Post.new(uri.path)
    req.set_form_data(params)
    req.initialize_http_header(headers)
    response = http.request(req)

    [response.body, response.code.to_i]
  end
end
```

## 1. JWTの作成
まず、Google Play Developer APIのサービスアカウントをJSONファイルで作成。
Roleは後からでも変更できるらしいので、権限は最低限を推奨。
https://developers.google.com/android-publisher/getting_started?hl=ja#using_a_service_account

JSONを開くとこのような構造になっているので、
https://cloud.google.com/iam/docs/creating-managing-service-account-keys

```json:api-hogehoge.json
{
  "type": "service_account",
  "project_id": "[PROJECT-ID]",
  "private_key_id": "[KEY-ID]",
  "private_key": "-----BEGIN PRIVATE KEY-----\n[PRIVATE-KEY]\n-----END PRIVATE KEY-----\n",
  "client_email": "[SERVICE-ACCOUNT-EMAIL]",
  "client_id": "[CLIENT-ID]",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/[SERVICE-ACCOUNT-EMAIL]"
}
```

client_emailとprivate_keyの文字列を取り出し、JWTに以下のように投入する。
※ この例はRailsなので`Rails.application.credentials`を経由している。環境に応じて投入方法を変えてください。

```ruby
  def service_auth_jwt
    payload = {
      iss: Rails.application.credentials.google_service_account_client_email,
      scope: 'https://www.googleapis.com/auth/androidpublisher',
      aud: 'https://oauth2.googleapis.com/token',
      exp: Time.now.to_i + 3600,
      iat: Time.now.to_i
    }
    rsa_private = OpenSSL::PKey::RSA.new(Rails.application.credentials.google_service_account_private_key)
    JWT.encode(payload, rsa_private, 'RS256')
  end
```

scopeはAPIのスコープのこと。
最初なんなのかわからなかったが、使用したいAPIのリファレンスに記載されていた。
https://developers.google.com/android-publisher/api-ref/purchases/products/get

JWT自体の作成方法はこういうものとしてスルーして大丈夫です。

## 2. アクセストークンの取得
まだAndroid Publisher APIは叩けません。
できたJWTを`https://oauth2.googleapis.com/token`に放り投げ、1時間有効のアクセストークンを取得する。
※ Railsログ出力の部分`Rails.logger.error`は環境によって変更もしくは削除してください。

```ruby
  def issue_request_token # [2]
    url = 'https://oauth2.googleapis.com/token'
    header = {
      'Content-Type' => 'application/x-www-form-urlencoded'
    }
    params = {
      assertion: service_auth_jwt, # [1]で作ったJWT
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'
    }
    body, status_code = post_request(url, header, params)
    if status_code >= 200 && status_code < 300
      res = JSON.parse(body)
      res['access_token']
    else
      Rails.logger.error('Could not fetch Google account access token')
      Rails.logger.error("status code: #{status_code}")
      Rails.logger.error("body: #{body}")
      nil
    end
  end
```

## 3. Android Publisher APIにリクエスト
アプリのパッケージ名、プロダクトID、アプリからPOSTされた検証用トークンをURLに埋め込み、
さらにヘッダに2で作成したアクセストークンを付与する。
※ パッケージ名は各自のものを書き込んで下さい。

```ruby
    base = "https://www.googleapis.com/androidpublisher/v3/applications/"
    url = base +  "パッケージネーム/purchases/products/#{product_id}/tokens/#{purchase_token}"
    res_string, status_code = get_request(url, access_token)
    # ...略
  end

  def get_request(url, token = nil)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme === 'https'

    req = Net::HTTP::Get.new(uri.path)
    req['Authorization'] = "Bearer #{token}" if token
    response = http.request(req)

    [response.body, response.code.to_i]
  end
```

## 4.購入情報の確認

3のリクエストに成功すると、このような検証結果が返ってくる。
https://developers.google.com/android-publisher/api-ref/purchases/products

```json
{
  "kind": "androidpublisher#productPurchase",
  "purchaseTimeMillis": long,
  "purchaseState": integer,
  "consumptionState": integer,
  "developerPayload": string,
  "orderId": string,
  "purchaseType": integer,
  "acknowledgementState": integer
}
```

よい検証を行う方法はいまひとつ説明されていないが、僕は
[1] `orderId`が存在する
[2] `purchaseState`が`0`(Purchased)
であれば購入済みと判断するコードにしました。

# 使用例
アプリから[検証用トークン](https://developer.android.com/reference/com/android/billingclient/api/Purchase.html#getpurchasetoken)と[プロダクトID](https://developer.android.com/reference/com/android/billingclient/api/Purchase.html#getSku())を送信、サーバ側で先程のクラスに渡す。
検証に成功したら[購入情報](https://developers.google.com/android-publisher/api-ref/purchases/products#resource)（レシート）が返ってくるので、プロダクトIDを元にアイテムを有効にしたり、DBに記録する。

```ruby
    # トークンとプロダクトIDを検証クラスに渡すと、購入情報が返ってくる
    transaction_id, purchase_data = ReceiptValidator.new.validate(receipt_data, product_id)

    if transaction_id
      # サーバ側で購入アイテムを有効にする処理、レシートを記録する処理など
    end
```

アプリ上のユーザアカウントとレシートを紐付けて記録しておくと、購入情報の再検証や復旧に役立ちます。
以上です。お疲れさまでした。