LoginSignup
2
3

More than 1 year has passed since last update.

Cloud FunctionsでiOSアプリ内課金のレシート検証を行う

Posted at

はじめに

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のコード属性と内容について

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3