Php/アプリストア連携/返金API/AppStore
提供: 初心者エンジニアの簡易メモ
目次
appleのstorekitの返金のドキュメント
https://developer.apple.com/jp/documentation/storekit/in-app_purchase/handling_refund_notifications/
ストアからの通知url設定箇所
appstore管理画面/配信/アプリ情報/appstoreサーバ通知
返金通知のサンプルJSON
- subtype: "DISPUTE", or "OTHER"
- signedRenewalInfo: 省略可能(テスト時)
- signedTransactionInfo: 省略可能(テスト時)
- cancellation_reason: 1:ユーザー申請, 0:その他
{ "notificationType": "REFUND", "subtype": "DISPUTE", // または "OTHER" "notificationUUID": "a1b2c3d4-5678-90ef-1234-567890abcdef", "data": { "appAppleId": 123456789, "bundleId": "com.example.app1", "bundleVersion": "1.0", "environment": "Sandbox", "signedRenewalInfo": "...", "signedTransactionInfo": "...", "unified_receipt": { "environment": "Sandbox", "latest_receipt": "BASE64_ENCODED_RECEIPT_DATA", "latest_receipt_info": [ { "cancellation_date_ms": "1625097600000", "cancellation_reason": "1", "product_id": "premium_subscription", "transaction_id": "1000000123456789", "original_transaction_id": "1000000123456789", "purchase_date_ms": "1625000000000", "expires_date_ms": "1627600000000" } ], "status": 0 } }, "version": "2.0" }
返金通知の実際のjwtデコードJSON
{ "notificationType": "SUBSCRIBED", "subtype": "INITIAL_BUY" "notificationUUID": "a1b2c3d4-5678-90ef-1234-567890abcdef", "data": { "appAppleId": 123456789, "bundleId": "com.example.app1", "bundleVersion": "202504201241", "environment": "Production", "signedRenewalInfo": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdByXjXWPNAT8g~略", "signedTransactionInfo": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWL9sbGXT55ZIi7Wt470x3w~略", "status": 1 } }, "version": "2.0", "signedDate": 1749043664833 }
appstore内の返金リクエストurl
ttps://reportaproblem.apple.com/
apple署名検証
jsonはjwtエンコードされてるので、jwtデコードする必要がある。 その際に、apple証明書で、検証する。アプリ別(bundle_id別)ではなく、どのアプリでも同じ証明書で検証する。
opensslでapple署名検証しながらfirebaseのjwtで、jwtデコードする
composerで、firebase/php-jwtのインストール
composer require firebase/php-jwt
apple署名検証付きデコード処理
use \Firebase\JWT\JWT; use \Firebase\JWT\Key; function decodeAppleJWS($signedPayload) { // 1. JWSのヘッダーを手動で解析(アルゴリズム確認用) $parts = explode('.', $signedPayload); $headerBase64 = $parts[0]; $header = json_decode(base64_decode($headerBase64), true); if ($header['alg'] !== 'ES256') { throw new Exception('Invalid algorithm. Expected ES256'); } // 2. Appleの証明書チェーンを準備 $leafCert = isset($header['x5c'][0]) ? $header['x5c'][0] : null; if (!$leafCert) { throw new Exception('Missing x5c certificate chain'); } // 3. 中間証明書とルート証明書をダウンロード $intermediateCert = file_get_contents('https://www.apple.com/certificateauthority/AppleWWDRCAG6.cer'); $rootCert = file_get_contents('https://www.apple.com/certificateauthority/AppleRootCA-G3.cer'); // 4. 証明書チェーンを検証 $certificateChain = array( 'leaf' => "-----BEGIN CERTIFICATE-----\n{$leafCert}\n-----END CERTIFICATE-----", 'intermediate' => $intermediateCert, 'root' => $rootCert ); if (!verifyCertificateChain($certificateChain)) { throw new Exception('Certificate chain validation failed'); } // 5. firebase/php-jwt でデコード // PHP 5系では新しい構文が使えないので、適切なJWTライブラリのバージョンを使用する必要あり return JWT::decode( $signedPayload, $certificateChain['leaf'], array('ES256') ); } // 証明書チェーン検証(簡略版) function verifyCertificateChain($chain) { // 1. 証明書をメモリ上で読み込み $cert = openssl_x509_read($chain['leaf']); if ($cert === false) { throw new Exception('Memory read failed: ' . openssl_error_string()); } // 2. 一時ファイルを作成 $tempFile = tempnam(sys_get_temp_dir(), 'cert_'); file_put_contents($tempFile, $chain['leaf']); // 3. 証明書検証(PHP 5系用の引数) $result = openssl_x509_checkpurpose( $tempFile, // 証明書ファイルパス X509_PURPOSE_ANY, // 目的 array(), // CAファイル(空でシステムデフォルトを使用) $tempFile // 検証対象証明書 ); // 4. リソース解放 openssl_x509_free($cert); unlink($tempFile); return (bool)$result; }