PHPのLaravelフレームワークを使用して、iOSの注文番号検証を実装した話
-
2022年11月8日
こんにちは。cocone connectでサーバー開発を担当しております S です。
今回は、PHPのLaravelフレームワークを使用して、iOSの注文番号検証を実装した話をしたいと思います。
弊社運用プロジェクトにて、社内ツールでiOSの注文番号を検証したいという要望があり、実装することになりました。
iOSの注文番号とは
iOSにおける「課金に紐づく番号」です。
iOS端末での課金の際、Appleから送信されるレシートメールや購入履歴から確認できます。
この注文番号は、お客様からのお問い合わせで課金未反映などの対応時に必要になります。
Look Up Order ID API
調査したところ、レシートの注文番号からアプリ内課金の検証に使用できるAPI(Look Up Order ID)がApp Store Server APIで提供されていることがわかりました。
本APIの仕様を一部ピックアップします。
- App Store Server APIは、リクエストをJSON Web Token(JWT)で署名する必要がある
- Look Up Order ID APIは、有効な注文番号だった場合、JSON Web Signature(JWS)で署名されたトランザクションデータ(JWSTransaction)を返す
JWTとは
JSON形式で表現された認証情報などをURL文字列などとして安全に送受信できるよう、符号化やデジタル署名の仕組みを規定した標準規格です。
IETFによってRFC 7519として標準化されています。
JWTの生成手順は下記の通りです。
各言語用にJWTのライブラリが用意されていました。
- JWTヘッダ作成
- JWTペイロード作成
- JWT署名
JWSTransactionとは
JWSTransactionは、App StoreによってJWS形式で署名されています。
IETFによってRFC 7515として標準化されています。
- JWSTransactionは「.」でヘッダー・ペイロード・署名のパートにで区切られている
- ペイロードには課金のトランザクション情報が格納されている
- ヘッダーにはJWSの署名を検証するための情報が格納されている
実装周り
使用した主なミドルウェアバージョンは下記の通りです。
| PHP | 8.1.2 |
| Laravel Framework | 8.83.16 |
| firebase/php-jwt | 6.2.0 |
FirebaseリポジトリにPHP用JWTエンコード/デコードのライブラリが用意されていたので、今回はこちらを使用しました。
Look Up Order ID APIを呼び出す部分です。
// JWT署名作成
$payload = array(
'iss' => %イシューID%,
'aud' => 'appstoreconnect-v1',
'iat' => %現在日時のタイムスタンプ%,
'exp' => %現在日時のタイムスタンプ% + 3600,
'bid' => %アプリのバンドルID%,
);
$token = JWT::encode($payload, %APIキー生成時のプライベートキー%, 'ES256', %プライベートキーID%);
// Look Up Order ID API呼び出し
$response = Http::withToken($token)->acceptJson()->get('https://api.storekit.itunes.apple.com/inApps/v1/lookup/' . %検証したい注文番号%);
Look Up Order ID APIの呼び出しが成功すると、レスポンスで署名されたトランザクションデータ($response[‘signedTransactions’])を返します。
そこには注文番号に紐づくトランザクションデータが配列で入っているので、それぞれのトランザクションデータ($transaction)に対して検証を行います。
// トランザクション内のヘッダ、ペイロードなどは「.」で区切られ、Base64エンコードされている
list($header, $payload, $signature) = explode('.', $transaction);
$header = JWT::jsonDecode(JWT::urlsafeB64Decode($header));
$payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payload));
// 証明書チェーンの最後の証明書がルート証明書と同じか
$last_cert_idx = count($header->x5c) - 1;
$last_cert = JWT::urlsafeB64Decode($header->x5c[$last_cert_idx]);
$root_cert = %「Apple Root」証明書を読み込み%;
if ($last_cert !== $root_cert) {
%エラー処理%;
}
// 証明書チェーンの検証
for ($cert_idx = 0; $cert_idx < $last_cert_idx; $cert_idx++) {
// 次の証明書の公開鍵を取得
$next_cert = %$header->x5cの「$cert_idx + 1」番目の証明書(pem形式)%;
$public_key = openssl_get_publickey($next_cert);
// 現在の証明書を次の証明書の公開鍵で検証
$current_cert = %$header->x5cの「$cert_idx」番目の証明書(pem形式)%;
$verify = openssl_x509_verify($current_cert, $public_key);
if ($verify !== 1) {
%エラー処理%;
}
}
// トランザクションデータを最初の証明書の公開鍵で検証
$first_cert = %$header->x5cの最初の証明書(pem形式)%;
$public_key = openssl_get_publickey($first_cert);
$decoded = JWT::decode($transaction, new Key($public_key, 'ES256'));
if ($decoded != $payload) {
%エラー処理%;
}
最後に
Laravelでは認証トークン付きのAPI呼び出しがとても簡単にできて便利でした。
躓いたポイントとしては「PHPで実装する場合、レスポンスに含まれている証明書をpem形式に変換する必要がある」という点です。
今回の実装を通じて、App Store Server APIの使い方や使用されている技術(JWTやJWSなど)についてとても勉強になりました。
参考サイト
- https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id
- https://developer.apple.com/documentation/appstoreserverapi/creating_api_keys_to_use_with_the_app_store_server_api
- https://developer.apple.com/documentation/appstoreserverapi/generating_tokens_for_api_requests
- https://www.rfc-editor.org/rfc/rfc7519
- https://e-words.jp/w/JWT.html
- https://developer.apple.com/documentation/appstoreserverapi/jwstransaction
- https://datatracker.ietf.org/doc/html/rfc7515
- https://github.com/firebase/php-jwt
- https://developer.apple.com/forums/thread/691464
cocone connectでは一緒に働く仲間を募集中です。
ご興味のある方は、こちらのリンクからぜひご応募ください。
cocone connect株式会社 採用情報
https://recruit.jobcan.jp/coconeconnect
cocone connect株式会社 公式サイト
また、ココネでも一緒に働く仲間を募集中です。
ご興味のある方は、ぜひこちらの採用特設サイトをご覧ください。