C#でのInAppPurchaseレシート検証処理(App Storeを使用)

# はじめに
iPhoneでのInAppPurchase(消耗型課金)を実装しました。
StoreKitによる購入完了後、サーバ側のアイテム購入処理を呼び出しています。

その際、不正利用を防ぐためサーバ側処理内でレシートの正当性検証をしています。
C#で実装したその検証部分を共有します。

こちらの実装を元に改変を行っています。
https://github.com/Redth/APNS-Sharp

レシート情報自体はBase64で受け取った想定です。

# レシートクラスとレシート検証処理クラス

### レシートクラス

Apple側サーバのレスポンスからレシート情報を取り出しています。
以下の例ではoriginal_transaction_idとproduct_idのみ取得しています。

```lang:Receipt.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;

namespace [namespace]
{
    [Serializable()]
    public class Receipt
    {
        public class InApp
        {
            public string quantity { get; set; }
            public string product_id { get; set; }
            public string transaction_id { get; set; }
            public string original_transaction_id { get; set; }
            public string purchase_date { get; set; }
            public string purchase_date_ms { get; set; }
            public string purchase_date_pst { get; set; }
            public string original_purchase_date { get; set; }
            public string original_purchase_date_ms { get; set; }
            public string original_purchase_date_pst { get; set; }
            public string is_trial_period { get; set; }
        }

        public List<InApp> in_app { get; set; }

        public int Status { get; set; }

        #region Constructor

        /// <summary>
        /// Creates the receipt from Apple's Response
        /// </summary>
        /// <param name="receipt"></param>
        public Receipt(string receipt)
        {
            JObject json = JObject.Parse(receipt);

            int status = -1;

            int.TryParse(json["status"].ToString(), out status);
            this.Status = status;

            if (this.Status != 0)
            {
                throw new Exception();
            }

            json = (JObject)json["receipt"];

            in_app = new List<InApp>();
            foreach (var token in json["in_app"].Children())
            {
                InApp inapp = new InApp();
                inapp.original_transaction_id = (string)token["original_transaction_id"];
                inapp.product_id = (string)token["product_id"];
                in_app.Add(inapp);
            }
        }

        #endregion Constructor

    }
}
```

### 検証処理クラス

```lang:ReceiptVerification.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using [Receiptクラスのnamespace];

namespace [namespace]
{
    public class ReceiptVerification
    {
        #region Constants
        private const string urlSandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
        private const string urlProduction = "https://buy.itunes.apple.com/verifyReceipt";
        #endregion

        #region Public Static Methods
        /// <summary>
        /// Sends the ReceiptData to the Verification Url to be verified.
        /// </summary>
        /// <returns>If true, the Receipt Verification Server indicates a valid transaction response</returns>
        public static bool IsReceiptValid(Receipt receipt)
        {
            return (receipt != null && receipt.Status == 0);
        }

        public static Receipt GetReceipt(bool sandbox, string receiptData)
        {
            return GetReceipt(sandbox ? urlSandbox : urlProduction, receiptData);
        }

        public static Receipt GetReceipt(string url, string receiptData)
        {
            Receipt result = null;

            string post = PostRequest(url, ConvertReceiptToPost(receiptData));

            if (!string.IsNullOrEmpty(post))
            {
                try { result = new Receipt(post); }
                catch { result = null; }
            }

            return result;
        }
        #endregion

        #region Private Static Methods

        /// <summary>
        /// Make a string with the receipt encoded
        /// </summary>
        /// <param name="receipt"></param>
        /// <returns></returns>
        private static string ConvertReceiptToPost(string receipt)
        {
            return string.Format(@"{{""receipt-data"":""{0}""}}", receipt);
        }

        /// <summary>
        /// Sends a request to the server and reads the response
        /// </summary>
        /// <param name="url"></param>
        /// <param name="postData"></param>
        /// <returns></returns>
        private static string PostRequest(string url, string postData)
        {
            byte[] byteArray = Encoding.UTF8.GetBytes(postData);
            return PostRequest(url, byteArray);
        }

        /// <summary>
        /// Sends a request to the server and reads the response
        /// </summary>
        /// <param name="url"></param>
        /// <param name="byteArray"></param>
        /// <returns></returns>
        private static string PostRequest(string url, byte[] byteArray)
        {
            try
            {
                WebRequest request = HttpWebRequest.Create(url);
                request.Method = "POST";
                request.ContentLength = byteArray.Length;
                request.ContentType = "text/plain";

                using (System.IO.Stream dataStream = request.GetRequestStream())
                {
                    dataStream.Write(byteArray, 0, byteArray.Length);
                    dataStream.Close();
                }

                using (WebResponse r = request.GetResponse())
                {
                    using (System.IO.StreamReader sr = new System.IO.StreamReader(r.GetResponseStream()))
                    {
                        return sr.ReadToEnd();
                    }
                }
            }
            catch
            {
                return string.Empty;
            }
        }

        #endregion Private Static Methods
    }
}
```

# 呼び出し部分
先に本番環境に投げ、取得失敗の場合にSandbox環境に投げています。

```lang:サーバ側購入処理クラスのメソッド内にて
                        // レシートデータの正当性検証
                        // 先に本番環境に投げ、取得失敗の場合、Sandbox環境へ投げる
                        Receipt receipt = ReceiptVerification.GetReceipt(false, base64ReceiptDataString);

                        if (!ReceiptVerification.IsReceiptValid(receipt))
                        {
                            receipt = ReceiptVerification.GetReceipt(true, base64ReceiptDataString);
                        }

                        if (!ReceiptVerification.IsReceiptValid(receipt))
                        {
                            // 処理終了
                            return -1;
                        }

                        // 処理の続き...
```

# おわりに
レシート正当性検証はAppleにレシート情報を送付することで実現しています。
Appleに送付せずに検証することも可能です。

その場合、こちらを参考にしてください。
https://developer.apple.com/jp/devcenter/ios/library/documentation/ValidateAppStoreReceipt.pdf

自分が実装する際に消耗型課金の実装例やC#でのサーバ側実装情報をなかなか見つけられなかったので、何かの参考になりましたら幸いです。