|
|
| (同じ利用者による、間の6版が非表示) |
| 行1: |
行1: |
| − | ==appleのstorekitの返金のドキュメント==
| + | [[Php/アプリストア連携/返金API/AppStore/v2]] |
| − | https://developer.apple.com/jp/documentation/storekit/in-app_purchase/handling_refund_notifications/
| + | |
| | | | |
| − | https://developer.apple.com/documentation/AppStoreServerNotifications/unified_receipt/Latest_receipt_info-data.dictionary
| + | [[Php/アプリストア連携/返金API/AppStore/v1]] |
| − | | + | |
| − | ==ストアからの通知url設定箇所==
| + | |
| − | appstore管理画面/配信/アプリ情報/appstoreサーバ通知
| + | |
| − | | + | |
| − | ==返金通知のサンプルJSON==
| + | |
| − | *subtype: "DISPUTE", or "OTHER"
| + | |
| − | *signedRenewalInfo: 省略可能(テスト時)
| + | |
| − | *signedTransactionInfo: 省略可能(テスト時)
| + | |
| − | *cancellation_reason: 1:ユーザー申請, 0:その他
| + | |
| − | <pre>
| + | |
| − | {
| + | |
| − | "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": "...",
| + | |
| − | "unifiedReceipt": {
| + | |
| − | "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"
| + | |
| − | }
| + | |
| − | </pre>
| + | |
| − | | + | |
| − | | + | |
| − | ==返金通知の実際の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
| + | |
| − | }
| + | |
| − | </pre>
| + | |
| − | | + | |
| − | ==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署名検証付きデコード処理
| + | |
| − | <pre>
| + | |
| − | use \Firebase\JWT\JWT;
| + | |
| − | use \Firebase\JWT\Key;
| + | |
| − | | + | |
| − | function verifyAppleJWSWithFirebaseJWT($signedPayload) {
| + | |
| − | // 1. JWSのヘッダーを手動で解析(アルゴリズム確認用)
| + | |
| − | [$headerBase64] = explode('.', $signedPayload);
| + | |
| − | $header = json_decode(base64_decode($headerBase64), true);
| + | |
| − |
| + | |
| − | if ($header['alg'] !== 'ES256') {
| + | |
| − | throw new Exception('Invalid algorithm. Expected ES256');
| + | |
| − | }
| + | |
| − | | + | |
| − | // 2. Appleの証明書チェーンを準備
| + | |
| − | $leafCert = $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 = [
| + | |
| − | '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 でデコード
| + | |
| − | $decoded = JWT::decode(
| + | |
| − | $signedPayload,
| + | |
| − | new Key($certificateChain['leaf'], 'ES256')
| + | |
| − | );
| + | |
| − | | + | |
| − | return $decoded;
| + | |
| − | }
| + | |
| − | | + | |
| − | // 証明書チェーン検証(簡略版)
| + | |
| − | function verifyCertificateChain($chain) {
| + | |
| − | // 1. 証明書をメモリ上で読み込み
| + | |
| − | $cert = openssl_x509_read($chain['leaf']);
| + | |
| − | if ($cert === false) {
| + | |
| − | throw new Exception('Memory read failed: ' . openssl_error_string());
| + | |
| − | }
| + | |
| − | | + | |
| − | // 2. 一時ファイルを作成(PHP 8.0+ の仕様に対応)
| + | |
| − | $tempFile = tempnam(sys_get_temp_dir(), 'cert_');
| + | |
| − | file_put_contents($tempFile, $chain['leaf']);
| + | |
| − |
| + | |
| − | // 3. 証明書検証(引数仕様に準拠)
| + | |
| − | $result = openssl_x509_checkpurpose(
| + | |
| − | $tempFile, // string 型で渡す
| + | |
| − | X509_PURPOSE_ANY,
| + | |
| − | [], // CAファイル(空でシステムデフォルトを使用)
| + | |
| − | $tempFile // 検証対象証明書
| + | |
| − | );
| + | |
| − |
| + | |
| − | // 4. リソース解放
| + | |
| − | openssl_x509_free($cert);
| + | |
| − | unlink($tempFile);
| + | |
| − |
| + | |
| − | return (bool)$result;
| + | |
| − | }
| + | |
| − | </pre>
| + | |