「Php/アプリストア連携/返金API/AppStore」の版間の差分
提供: 初心者エンジニアの簡易メモ
| 行84: | 行84: | ||
function decodeAppleJWS($signedPayload) { | function decodeAppleJWS($signedPayload) { | ||
// 1. JWSのヘッダーを手動で解析(アルゴリズム確認用) | // 1. JWSのヘッダーを手動で解析(アルゴリズム確認用) | ||
| − | + | $parts = explode('.', $signedPayload); | |
| + | $headerBase64 = $parts[0]; | ||
$header = json_decode(base64_decode($headerBase64), true); | $header = json_decode(base64_decode($headerBase64), true); | ||
| 行92: | 行93: | ||
// 2. Appleの証明書チェーンを準備 | // 2. Appleの証明書チェーンを準備 | ||
| − | $leafCert = $header['x5c'][0] ? | + | $leafCert = isset($header['x5c'][0]) ? $header['x5c'][0] : null; |
if (!$leafCert) { | if (!$leafCert) { | ||
throw new Exception('Missing x5c certificate chain'); | throw new Exception('Missing x5c certificate chain'); | ||
| 行102: | 行103: | ||
// 4. 証明書チェーンを検証 | // 4. 証明書チェーンを検証 | ||
| − | $certificateChain = | + | $certificateChain = array( |
'leaf' => "-----BEGIN CERTIFICATE-----\n{$leafCert}\n-----END CERTIFICATE-----", | 'leaf' => "-----BEGIN CERTIFICATE-----\n{$leafCert}\n-----END CERTIFICATE-----", | ||
'intermediate' => $intermediateCert, | 'intermediate' => $intermediateCert, | ||
'root' => $rootCert | 'root' => $rootCert | ||
| − | + | ); | |
if (!verifyCertificateChain($certificateChain)) { | if (!verifyCertificateChain($certificateChain)) { | ||
| 行113: | 行114: | ||
// 5. firebase/php-jwt でデコード | // 5. firebase/php-jwt でデコード | ||
| + | // PHP 5系では新しい構文が使えないので、適切なJWTライブラリのバージョンを使用する必要あり | ||
return JWT::decode( | return JWT::decode( | ||
$signedPayload, | $signedPayload, | ||
| − | + | $certificateChain['leaf'], | |
| + | array('ES256') | ||
); | ); | ||
} | } | ||
| 行127: | 行130: | ||
} | } | ||
| − | // 2. | + | // 2. 一時ファイルを作成 |
$tempFile = tempnam(sys_get_temp_dir(), 'cert_'); | $tempFile = tempnam(sys_get_temp_dir(), 'cert_'); | ||
file_put_contents($tempFile, $chain['leaf']); | file_put_contents($tempFile, $chain['leaf']); | ||
| − | // 3. | + | // 3. 証明書検証(PHP 5系用の引数) |
$result = openssl_x509_checkpurpose( | $result = openssl_x509_checkpurpose( | ||
| − | $tempFile, // | + | $tempFile, // 証明書ファイルパス |
| − | X509_PURPOSE_ANY, | + | X509_PURPOSE_ANY, // 目的 |
| − | + | array(), // CAファイル(空でシステムデフォルトを使用) | |
| − | $tempFile | + | array($tempFile) // 検証対象証明書(配列形式) |
); | ); | ||
2025年6月9日 (月) 18:29時点における版
目次
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ファイル(空でシステムデフォルトを使用)
array($tempFile) // 検証対象証明書(配列形式)
);
// 4. リソース解放
openssl_x509_free($cert);
unlink($tempFile);
return (bool)$result;
}
