AppStore#verifyReceiptの代替対応

## はじめに
この記事は[フリューAdvent Calendar 2023](https://qiita.com/advent-calendar/2023/furyu)の22日目の記事となります。

フリュー株式会社スマートフォンゲーム部で、Webサーバーエンジニアをしている山根です。
AppStoreの消費型の課金処理で、レシートの検証として
[AppStore#verifyReceipt](https://developer.apple.com/documentation/appstorereceipts/verifyreceipt)
を使ってきましたが、ついに非推奨になってしまいました。
これに代わる対応について紹介していきたいと思います。

## 代替案
ドキュメントには、以下3つの代替案が記述されています。
1. [AppStoreServerAPI](https://developer.apple.com/documentation/appstoreserverapi)を使って、Apple署名付きトランザクション情報を取得、検証する
1. アプリが取得するAppTransactionおよびTransactionの署名付きトランザクション情報を検証する
1. AppStoreServerNotificationsV2のエンドポイントから署名付きトランザクション情報を取得、検証する

今回は、これらの中で、
1. [AppStoreServerAPI](https://developer.apple.com/documentation/appstoreserverapi)を使って、Apple署名付きトランザクション情報を取得、検証する

について紹介します。

## AppStoreServerAPIを使った検証
ここでは、Appleが用意しているJava用のライブラリを使って`Kotlin`で実装をしていきます。

https://github.com/apple/app-store-server-library-java

まず、[AppStoreServerAPI#Authorize your API calls](https://developer.apple.com/documentation/appstoreserverapi#3814333)に記載されているAPIキーをAppStoreConnectから事前に取得しておく必要があるので、準備しておきます。

準備ができたら、以下のように実装し、AppStoreServerAPIを使えるようにします。
```
val signingKey = // AppStoreConnectからダウンロードしたAPIキー
val keyId = // AppStoreConnectから取得したKeyId
val issuerId = // AppStoreConnectから取得したIssuerId
val bundleId = // AppのbundleId
val environment = // 対象となる環境(SANDBOX or PRODUCTION)

fun createAppleStoreServerAPIClient(): AppStoreServerAPIClient {
    return AppStoreServerAPIClient(signingKey, keyId, issuerId, bundleId, environment)
}    
```
今回の題目にあるAppStore#verifyReceiptの代わりとして、
[Get Transaction Info
](https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info)を使ってレシートの情報を取得したいと思います。

```
// transactionIdは、検証したいレシートに紐づくトランザクションID
fun getTransactionInfo(transactionId: String): TransactionInfoResponse {
    return createAppleStoreServerAPIClient().getTransactionInfo(transactionId)
}
```
これにより、JSON Web Signature (JWS)形式のトランザクション情報を取得できました。
これをさらに検証し、デコードしていきます。
ここの検証とデコードには、[SignedDataVerifier](https://github.com/apple/app-store-server-library-java/blob/main/src/main/java/com/apple/itunes/storekit/verification/SignedDataVerifier.java)を使っていきます。
これを使うにあたり、[Appleが用意しているルート証明書](https://www.apple.com/certificateauthority/)が必要になります。
今回は、Apple Root CA - G3を使っています。
SignedDataVerifierを使うことで、以下のように検証、デコードができます。
```
val appleRootCAFile = // Appleが用意しているルート証明書
val bundleId = // AppのbundleId
val appAppleId = // AppのId
val environment = // 対象となる環境(SANDBOX or PRODUCTION)

fun signedPayloadVerifier(): SignedDataVerifier {
    return SignedDataVerifier(
        setOf(appleRootCAFile.inputStream()),
        bundleId,
        appAppleId,
        environment,
        true
    )
}

fun getTransactionInfo(transactionId: String): JWSTransactionDecodedPayload {
    // transactionIdに紐づく情報を取得
    val transactionInfo = createAppleStoreServerAPIClient().getTransactionInfo(transactionId)
    return signedPayloadVerifier().verifyAndDecodeTransaction(transactionInfo.signedTransactionInfo)
}
```

これによって、`transactionId`に紐づくレシート情報が取得でき、
AppStore#verifyReceiptの代わりとして使うことができます。

## AppStoreServerAPIを使う上での注意点
AppStoreServerAPIは、1時間内に各エンドポイントに送信できるリクエスト数に制限が設定されています。

https://developer.apple.com/documentation/appstoreserverapi/identifying_rate_limits

今回の[Get Transaction Info
](https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info)であれば、1秒間で50リクエストとあるので、
1時間では、180,000リクエストまでとなっています。
(このリクエスト数は、Appleがいつでも変える可能性があるようです。)
そのため、このリクエスト制限に引っかかった場合のハンドリングが必要になります。
リクエスト制限に引っかかった場合は、`HTTP 429`が返ってきます。
また、`Retry-After`ヘッダーに、次にリクエストを送信できるUNIX時間をミリ秒単位で含んでいるため、それらを使ってハンドリングする感じになると思います。

## まとめ
AppStore#verifyReceiptの代わりとして、AppStoreServerAPIを使った検証を紹介しました。
~~今回使っているライブラリは、現時点ではベータ版となっていることから商用利用では注意が必要です。
Appleさんには、AppStore#verifyReceiptの利用停止までには正規版としてリリースしてくれることを願っております。~~（正規版がリリースされたようです。）