facebook twitter hatena line email

「Php/アプリストア連携/返金API/AppStore」の版間の差分

提供: 初心者エンジニアの簡易メモ
移動: 案内検索
 
(同じ利用者による、間の4版が非表示)
行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": "...",
+
    "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"
+
}
+
</pre>
+
 
+
 
+
==返金通知の実際のjwtデコードJSON==
+
<pre>
+
{
+
  "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 decodeAppleJWS($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 でデコード
+
    return JWT::decode(
+
        $signedPayload,
+
        new Key($certificateChain['leaf'], 'ES256')
+
    );
+
}
+
 
+
// 証明書チェーン検証(簡略版)
+
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>
+

2025年6月28日 (土) 03:15時点における最新版

Php/アプリストア連携/返金API/AppStore/v2

Php/アプリストア連携/返金API/AppStore/v1