[secret sauce] มาทำระบบ receipt verification in-app purchase กันเถอะ

Mr.Ess
te<h @TDG
Published in
4 min readJul 21, 2021

เคยไหมครับ เราอยากสร้างรายได้ให้ application ของเราด้วยการทำ in-app purchase ที่หักเงินผ่าน google android หรือ apple เนี่ย และเราไม่อยากเชื่อใจ client หรือใครหน้าไหน ที่อาจจะปลอมแปลง transection มาให้ระบบเรา แต่ถึงเวลาจะทำจริง ๆ กลับหาแหล่งความรู้อ่านไม่ได้เลยว่าเค้าออกแบบระบบกันยังไงนะ ถึงทำไปก็อาจจะโดนโกงเละเทะก็ได้ จะมีก็แค่ official document ที่เป็นรายละเอียดอันแสนจะปวดหัวว …

วันนี้จะมาเผยความลับนั้นให้ทุกคนได้รู้พร้อมกันครับ ><

มาดู High level ของระบบกันก่อนดีกว่า

การออกแบบระบบอาจจะแตกต่างกัน แล้วแต่การปรับใช้ ครับ แต่ในส่วน verify receipt หวังว่าจะเป็นประโยชน์ไม่มากก็น้อยนะครับ

เล่าคร่าว ๆ

เคยเติมเงินเกมหรือซื้อของผ่าน mobile app กันไหมครับ รู้หรือไม่ว่าจริง ๆ แล้วราคาที่แสดงเป็นราคาที่ application ดึงมาแสดงโดยตรงจาก store ตามที่เราตั้งชื่อและราคาไว้ (เช่น com.companyname.somecode = 29 บาท) ซึ่ง process การจ่ายเงินจะทำรายการโดยตรงระหว่าง application กับ store ตาม package id ที่ซื้อ จากนั้น client จะได้รับ receipts มา โดยทั่วไปหากเป็นระบบที่ไม่จริงจังมาก หรือ application ออฟไลน์เล็ก ๆ ก็คงจะจบแค่ตรงนี้ อย่างมากก็ validate ที่ client ( ที่ไม่ควรอย่างยิ่งคือ call store verify ผ่าน client // โดน apple ตบ) คราวนี้อย่างที่กล่าวไป การทำแค่นี้อาจจะโดน ปลอมแปลง transection หรือ Fake Purchases จากผู้ไม่หวังดีได้

เราจึงต้องส่ง receipts payload ไป verify ที่ server อีกครั้งนึง ถ้า verify ผ่านเราจึงจะให้สินค้าที่ซื้อ ตาม store product_id (com.xxx.xxx) อีกทีนึง อาจจะ เป็นการ map id ในระบบเรา กับ store product_id เพื่อให้รู้ว่า product_id นี้คือของอะไร แต่ก็ขึ้นอยู่กับการออกแบบระบบของเราอีกนั้นแหละครับ

In-app purchase มีกี่ประเภท

1. non-consumable : ซื้อได้ครั้งเดียว
2. consumable : ซื้อซ้ำได้
3. auto-renewable subscription : subscription รายวัน, 7 วัน หรือ รายเดือน รายปี
4. non-renewing subscriptions : subscription ที่ต้องกดต่ออายุเอง
ซึ่งวิธีการ verify จะไม่ต่างกันมาก แต่วิธีการจัดการ subscription หลังจาก verify จะแตกต่างกัน เช่น หมดอายุหรือยัง ต่ออายุไหม หรือยกเลิกไปแล้วนะ

จุดประสงค์ที่ต้องคำนึงถึง

1. Fake Purchases สาเหตุหลักที่เราต้องนำ receipt ที่ได้จาก client มา verify กับ Apple, Google โดยตรงก็เพื่อให้ไม่สามรถดักจับ และดัดแปลงได้

2. Replay Attacks ป้องกันการนำ receipt เดิมที่ verify ผ่านนำมา verify ซ้ำ หากไม่ป้องกันไว้ ถ้ามีการยิง verify มาซ้ำในระบบเรา(ทั้งจงใจและไม่จงใจ) ระบบอาจจะให้ product ซ้ำ กลายเป็นการปั้มของไปซะงั้น

3. Product Mismatches payload ของแต่ละ receipt มันจะ exposes data (เช่น product ID ) ฉะนั้นเพื่อป้องกันการแนบ product ID ที่มีมูลค่าสูงกว่าอันที่ซื้อจริง ๆ เราจะต้องใช้ product ID ที่ผ่านการ verify เท่านั้น

ขั้นตอนการ setup และรายละเอียด

ก่อนที่เราจะ verify ได้จะต้องมี key ไว้ใช้ในการยิง api ไป verify กับ store ก่อน ซึ่งวิธีการได้มาของแต่ละ store ก็จะแตกต่างกันดังนี้

App Store

Note: Apple ได้ให้ suggested ไว้ว่า
เราควรจะ Verify ทั้ง Production และ Sandbox นั้นแหละโดยที่ verify ไปที่ production URL ของ apple ก่อน ถ้าได้ status receive มาเป็น `21007` ก็ให้ verify ที่ sandbox URL ต่อได้เลย ทั้งนี้ก็เพื่อจะได้ไม่ต้องสลับ URLs ระหว่างเราเทส, หรือ รีวิวโดย apple หรือ แม้กระทั้ง live in app store แล้วนั้นเอง

URL
POST https://buy.itunes.apple.com/verifyReceipt

URL for Sandbox Testing

POST https://sandbox.itunes.apple.com/verifyReceipt

api ดังกล่าวจะต้องส่ง password (app’s shared secret) ไปด้วยถ้าต้องการ validate purchase ประเภท subscription หากไม่ต้องการก็ข้ามไปได้เลย (optional)
ใช้ได้ทั้ง production และ sandbox

วิธีการสร้าง app’s shared secret
ไปที่ App Store Connect เมนู In-App Purchases management จะอยู่ภายใต้ application ของเรา

หลังจาก generate แล้วจะได้หน้าตาแบบนี้

คราวนี้เราก็จะได้สิ่งที่ต้องการครบแล้วละ

เราจะมาดูแค่ตรง verify service กันนะครับ

service ตัวนี้เราจะทำการนำ payload ที่ client ส่งมานำไป post verify กับ store โดยตรง ซึ่งรายละเอียด. spec ของ apple มีดังนี้

Content-Type: application/json
HTTP POST request method

Properties

receipt-data [byte] (Required) The Base64-encoded receipt data.

password [string] (optional but subscription type is required) app’s shared secret จาก ขั้นตอนก่อนหน้า

exclude-old-transactions [boolean] (optional) Set เป็น false ถ้าต้องการ transaction เก่า มีผลเฉพาะการซื้อประเภท auto-renewable subscriptions เท่านั้น หากไม่ set มันจะ default เป็น true จ้า

ตัวอย่าง

เราจะได้ response Body กลับมาดังนี้

ดูเพิ่มเติม https://developer.apple.com/documentation/appstorereceipts/responsebody

สิ่งที่เราจะเน้นคือการจัดการกับ ประเภท consumable /non-consumable ก่อนนะครับ ส่วน subscriptions จะมีรายละเอียดปลีกย่อยอีก…แค่คิดก็เหนื่อยแล้ว ฮ่า

เรามาดู response field ที่น่าสนใจกัน

is-retryable [boolean] เพราะว่า apple มีโอกาสที่ช่วงเวลานึงจะไม่สามารถ verify ได้ หรือ verify ไม่สำเร็จแล้วมี is-retryable โผล่มา ถ้า value เป็น true เราจะต้องลอง verify ใหม่ภายหลัง

status [status] code 0 คือ success

payload จาก testflight (sandbox) post validate ไป production url

อีกอันที่น่าสนใจคือ code 21007 หมายความว่า receipt นี้มาจาก sandbox environment ให้เราไป verify ต่อที่ sandbox URL ตามคำแนะนำของ apple

receipt responseBody.Receipt field นี้มีสิ่งที่เราสนใจอยู่ด้านใน คือ in_app

|___ in_app [responseBody.Receipt.In_app] field นี้ จะ contains array transactions ซึ่งสิ่งที่เราต้องเช็คคือ
- check array In_app ว่า empty ไหมถ้า empty แสดงว่า store ไม่ได้ charge หรือ charge ยังไม่สำเร็จจริง
- ดู transaction_id และ product_id นำไป verify กับระบบเราต่อ ว่า transaction_id ซ้ำหรือไม่ (ป้องกัน Replay Attacks) และนำ product_id ไปใช้ในระบบของเราต่อไป

สำหรับ verify ของ apple ก็น่าจะประมาณนี้ จริง ๆ แล้วยังมีรายละเอียดอีก ถ้าหากเราจะทำ purchase ประเภท subscriptions หรือจัดการเกี่ยวกับ เรื่อง refund

เอาไว้ blog ถัด ๆ ไปจะมาต่อรายละเอียดเรื่องนี้

สำหรับ part II จะเป็น verify ของ ​google บ้างครับ สามารถใช้ flow เดียวกันได้เลยแต่จะแตกต่างตรงที่ google android จะมีเรื่องของ oauth มาเกี่ยวข้องครับ

--

--