はじめに
App StoreのレシートをApp StoreサーバーのverifyReceiptエンドポイントで検証をする際、安全なサーバーから行うように公式ドキュメントで警告されています。
AppからApp StoreサーバーのverifyReceipt(英語)エンドポイントを呼び出さないでください。接続のどちらの終端もコントロールすることができず、中間者攻撃を受けやすくなるため、ユーザーのデバイスとApp Storeとの間で信頼できる接続を直接構築することができません。
App Storeを使用してレシートを検証する - 日本語ドキュメント - Apple Developer
Cloud Functionsからでも代用できるので、今回はCloud FunctionsからApp Storeのレシート検証を行う方法をまとめていきます。
クライアント
クライアントからはbase64にエンコードされたレシートデータと一緒に、Cloud FunctionsのhttpsCallable関数を呼び出します。
const receiptVerification = firebase.functions().httpsCallable('receiptVerification');
try {
const response = await receiptVerification({
receipt // base64にエンコードされたレシートデータ
})
} catch (error) {
console.error(`error: ${error}`)
}
Cloud Functions
httpsCallable関数の中身はこのようになります。
export const receiptVerification = functions.https.onCall(async ({ receipt }) => {
let i = 0;
let verificationEnv = 'buy';
// 再試行が求められるステータスコード
const retryStatuses = [21002, 21005, 21009]
// レシート検証のリクエストBody
const body = JSON.stringify({
'receipt-data': receipt,
password: '***', // 共有シークレット
});
do {
const response = await axios.post(
`https://${verificationEnv}.itunes.apple.com/verifyReceipt`,
body
);
if (response.data.status === 0) {
// レシート検証が成功
return 'success';
} else if (response.data.status === 21007) {
// テスト環境のレシートを本番に送信。エンドポイントをsandboxに変更して再試行
verificationEnv = 'sandbox';
} else if (!retryStatuses.includes(response.data.status)) {
// 再試行が求められるステータスコード以外はエラーを返す
throw new functions.https.HttpsError('invalid-argument', 'レシート検証に失敗しました。');
}
if (i === 3) {
// 3回試行した場合はエラーを返す
throw new functions.https.HttpsError('invalid-argument', 'レシート検証に失敗しました。');
}
i++;
} while (i < 4);
throw new functions.https.HttpsError('invalid-argument', 'レシート検証に失敗しました。');
});
ステータスコートと再試行
App Storeサーバーへのレシート検証は、ステータスコードによって再試行が求められます。例えば、21005の「レシートサーバーが一時的に利用できなくなった場合」などです。このようなケースに備え、verifyReceiptエンドポイントへのリクエストは3回まで再試行するようにdo...while文を使用しています。
App Storeサーバーからのレスポンスステータスの一覧をまとめると、以下のようになります。
ステータスコード | 内容 |
---|---|
0 | 有効なレシートです。 |
21000 | App StoreへのリクエストがHTTP POSTリクエストメソッドを使用して行われませんでした。 |
21001 | これ以上App Storeから送信されなくなりました。 |
21002 | receipt-dataプロパティのデータの形式が正しくないか、サービスで一時的な問題が発生しました。再試行してください。 |
21003 | レシートの認証ができませんでした。 |
21004 | リクエストの共有シークレットがアカウントに登録されている共有シークレットと一致しませんでした。 |
21005 | レシートサーバーが一時的に利用できなくなりました。再試行してください。 |
21006 | レシートは有効ですがサブスクリプションの有効期限が切れています。 |
21007 | テスト環境のレシートが本番環境に送信されました。 |
21008 | 本番環境のレシートがテスト環境に送信されました。 |
21009 | 内部データのアクセスエラーが発生しました。再試行してください。 |
21010 | アカウントが見つからないか、削除されました。 |
今回は、ステータスコードが21002, 21005, 21009の場合にのみ再試行するよう条件分岐をしています。
// 再試行が求められるステータスコード
const retryStatuses = [21002, 21005, 21009]
do {
const response = await axios.post(
`https://${verificationEnv}.itunes.apple.com/verifyReceipt`,
body
);
if (!retryStatuses.includes(response.data.status)) {
// 再試行が求められるステータスコード以外はエラーを返す
throw new functions.https.HttpsError('failed-precondition', 'エラーが発生しました');
}
if (i === 3) {
// 3回試行した場合はエラーを返す
throw new functions.https.HttpsError('invalid-argument', 'レシート検証に失敗しました。');
}
i++;
} while (i < 4);
本番環境とテスト環境(Sandbox)のエンドポイント
App StoreサーバーのverifyReceiptエンドポイントは、本番環境とテスト環境(Sandbox)で異なります。
レスポンスコード21008のように、本番環境のレシートがテスト環境に送信されたことがレスポンスから分かります。今回は、初めに本番環境のエンドポイントへレシート検証を行い、21008のレスポンスが返ってきた場合に、テスト環境へのエンドポイントへ再度レシート検証を行うようにします。
let verificationEnv = 'buy';
const response = await axios.post(
`https://${verificationEnv}.itunes.apple.com/verifyReceipt`,
body
);
if (response.data.status === 21007) {
// テスト環境のレシートを本番に送信。エンドポイントをsandboxに変更して再試行
verificationEnv = 'sandbox';
}
リクエストBodyの中身
App StoreサーバーへのリクエストBodyには、base64にエンコードされたレシートデータと共有シークレットが必要です。レシートデータはクライアント側から受け取り、共有シークレットはApp Store Connect で生成される、32 個の英数字からなる 16 進数文字列です。
この内容をaxiosを用いてリクエストを送ります。
// レシート検証のリクエストBody
const body = JSON.stringify({
'receipt-data': receipt,
password: '***', // 共有シークレット
});
const response = await axios.post(
`https://${verificationEnv}.itunes.apple.com/verifyReceipt`,
body
);
以上が、Cloud FunctionsでiOSアプリ内課金のレシート検証を行う方法についてでした。
httpsCallable関数でエラーを返す方法はこちらでまとめているので、参考にしてみてください。
Cloud FunctionsのHttpsErrorのコード属性と内容について