สร้างแอป Shopify ฝังตัวด้วย Symfony: คู่มือเจาะลึกความปลอดภัยและ Token Exchange ใน PHP
เจาะลึกวิธีการพัฒนาแอปพลิเคชัน Shopify แบบ Embedded บน Symfony 7.4 และ PHP 8.5 ตั้งแต่การจัดการ Session Token, Token Exchange ไปจนถึงส่วนที่เอกสารทางการไม่ค่อยพูดถึง

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
- นักพัฒนาที่ใช้ภาษาอื่นนอกเหนือจาก Node ต้องเผชิญความท้าทายในการจัดการแอปฝังตัวใน iframe
- แอป Shopify ฝังตัวไม่สามารถใช้ PHP session ได้ จึงต้องพึ่งพา Session Token แบบ JWT ที่มีอายุเพียง 1 นาที
- การแลก Token ใช้มาตรฐาน OAuth 2.0 token exchange (RFC 8693) แทนการทำ authorization-code แบบเดิม
- บทความนี้นำเสนอโค้ดจริงจากการใช้งานจริงในแอป StockPilot บน Symfony 7.4 และ PHP 8.5
เอกสารทางการของ Shopify มักจะเน้นเส้นทางหลักด้วย Node, Official CLI และเทมเพลต Remix เป็นหลัก ทำให้นักพัฒนาที่เลือกใช้ภาษาอื่นต้องเผชิญกับความท้าทายตั้งแต่ก้าวแรก เนื่องจากแอปพลิเคชันของคุณจะรันอยู่ใน iframe ภายในหน้าผู้ดูแลระบบของ Shopify ซึ่งเบราว์เซอร์จะไม่ยอมส่งคุกกี้ข้ามเว็บไซต์ให้ และ Token ที่จำเป็นต่อการเรียกใช้งาน Admin API ก็จะต้องถูกนำมาแลกเปลี่ยนกันแบบสด ๆ
เรื่องเหล่านี้ไม่ได้ยากเกินความสามารถ แต่มันแค่ขาดเอกสารอธิบายสำหรับนอกเหนือจากฝั่ง JavaScript เท่านั้น บทความนี้จึงรวบรวมเส้นทางด้านความปลอดภัยและการยืนยันตัวตนฉบับสมบูรณ์สำหรับการพัฒนาแอปพลิเคชัน Shopify แบบฝังตัวด้วย Symfony 7.4 และ PHP 8.5 ซึ่งนำมาจาก StockPilot แอปพลิเคชันจริงที่ผ่านการตรวจสอบบน Shopify App Store และเปิดให้บริการอยู่ในปัจจุบัน โดยทุกชุดโค้ดด้านล่างนี้คือโค้ดสำหรับใช้งานจริง ไม่ใช่เพียงโค้ดจำลอง
แอปพลิเคชันแบบฝังตัว (Embedded app) คือหน้าเว็บที่แสดงผลอยู่ภายใน admin.shopify.com ผ่าน iframe แบบ cross-origin ในบริบทนี้ Third-party cookies ไม่สามารถใช้งานได้อีกต่อไป ส่งผลให้ PHP session ไม่สามารถใช้งานได้และไม่มีวันใช้งานได้ คำตอบของ Shopify คือ JWT ระยะสั้นที่เรียกว่า Session Token ซึ่งสร้างโดย App Bridge (ไลบรารี JavaScript ของ Shopify ที่โหลดจาก CDN) ในทุก ๆ คำขอ โดยโทเค็นนี้จะมีอายุการใช้งานเพียง 1 นาทีและถูกแนบไปกับทุกคำขอ fetch() ภายในเว็บไซต์
สถาปัตยกรรมนี้บังคับให้เกิดการแบ่งส่วนที่นักพัฒนา PHP มักจะไม่ค่อยทำโดยปริยาย:
- ส่วนเชลล์ (Shell) และหน้าต่างผู้ดูแลระบบ: ต้องให้บริการอย่างรวดเร็ว, มีการแคชได้ง่าย และไม่ต้องใช้การยืนยันตัวตนในฝั่งเซิร์ฟเวอร์
- ส่วนคำขอ API ของแอป: เป็นส่วนที่ stateless ทั้งหมด และต้องทำการตรวจสอบความถูกต้องของ Session Token ในทุก ๆ คำขอที่เข้ามา
การแบ่งส่วนนี้ช่วยพลิกสัญชาตญาณดั้งเดิมของ Symfony ที่มักจะป้องกันตัวคอนโทรลเลอร์และเรนเดอร์ข้อมูลจากฝั่งเซิร์ฟเวอร์ โดยในกรณีนี้ การเรนเดอร์ข้อมูลลงในเชลล์จะหมายถึงการยืนยันตัวตนในขณะโหลดหน้าที่ไม่มีข้อมูลประจำตัวใด ๆ ส่งผลให้เชลล์สามารถแคชได้อย่างง่ายดายและมีขอบเขตความปลอดภัยที่ชัดเจนผ่านไฟร์วอลล์เพียงชุดเดียว
การกำหนดค่าใน config/packages/security.yaml จะมีรูปแบบดังนี้:
- webhooks: ตรวจสอบความถูกต้องผ่านลายเซ็น HMAC ภายในตัวคอนโทรลเลอร์ แทนที่จะใช้ไฟร์วอลล์
- api: สำหรับแอปพลิเคชัน Admin API ที่ฝังตัว ใช้ App Bridge session token บนทุกคำขอแบบ stateless
- main: ตั้งค่าเป็น lazy และใช้ผู้ให้บริการข้อมูลผู้ใช้งานในหน่วยความจำ
Session Token คือ JWT ที่ลงนามด้วยอัลกอริทึม HS256 โดยใช้ Client Secret ของแอปพลิเคชัน การตรวจสอบลายเซ็นเป็นขั้นตอนที่ทุกคนทำกัน แต่การตรวจสอบ Claim 4 ส่วนหลังจากนั้นมักจะถูกละเลย ซึ่งแต่ละส่วนช่วยปิดช่องโหว่สำคัญ ได้แก่ การตรวจสอบ aud เพื่อป้องกันการยอมรับโทเค็นจากแอปอื่น และการตรวจสอบ iss กับ dest เพื่อป้องกันโทเค็นที่ระบุร้านค้าไม่ตรงกัน
การทำความเข้าใจสถาปัตยกรรมความปลอดภัยของ Shopify บนเฟรมเวิร์กอย่าง Symfony ถือเป็นกุญแจสำคัญในการสร้างแอปพลิเคชันที่ผ่านมาตรฐานความปลอดภัยระดับสูงของ App Store การที่ Shopify บังคับใช้ stateless API และการตรวจสอบ JWT อย่างเข้มงวดช่วยป้องกันช่องโหว่ประเภท CSRF และการโจมตีแบบ cross-origin ได้อย่างมีประสิทธิภาพ ซึ่งนักพัฒนา PHP จำเป็นต้องปรับเปลี่ยนแนวคิดจากการใช้เซสชันแบบเดิมมาเป็นการจัดการโทเค็นระยะสั้นแทน
Session Token เป็นเพียงเครื่องยืนยันตัวตนว่าใครกำลังร้องขอข้อมูล แต่ยังไม่เพียงพอต่อการเรียกใช้งาน Admin API โดยคุณจะต้องนำมันไปแลกเปลี่ยนเป็น Access Token ผ่านมาตรฐาน OAuth 2.0 token exchange (RFC 8693) แทนการใช้งาน authorization-code แบบเดิมที่มีการเปลี่ยนเส้นทาง (Redirect) ด้วยระบบติดตั้งแบบจัดการเองนี้ จะทำให้ขั้นตอนการติดตั้งทั้งหมดไม่มีเส้นทาง /auth, ไม่มีหน้าเปลี่ยนเส้นทาง และไม่มี callback โดยคำขอแรกที่ผ่านการยืนยันตัวตนจากร้านค้าใหม่จะทำการแลกเปลี่ยนโทเค็นทันที
ในส่วนของ Webhooks จะไม่มีการแนบ Session Token มาด้วย แต่จะใช้การลงนามแบบ HMAC-SHA256 บนเนื้อหาคำขอแบบดิบ (Raw request body) และเข้ารหัส base64 ไว้ในเฮดเดอร์ X-Shopify-Hmac-Sha256 โดยคำว่าดิบหมายถึงข้อมูลก่อนผ่านการแปลงเป็น JSON หรือมิดเดิลแวร์ใด ๆ ทั้งสิ้น ซึ่งการตรวจสอบในส่วนนี้สำคัญมากเพราะการตรวจสอบอัตโนมัติของ Shopify App Store จะส่งเว็บฮุกที่จงใจใส่ลายเซ็นผิดมาทดสอบและต้องการผลลัพธ์เป็นรหัส 401
นอกจากนี้ยังมีข้อกำหนดด้าน Content Security Policy (CSP) ในส่วนของ frame-ancestors ที่ต้องถูกคำนวณแบบรายคำขอจากพารามิเตอร์ของร้านค้า และข้อกำหนดที่สำคัญอีกข้อที่มักมองข้ามในเลย์アウトแบบ Twig คือ App Bridge จะต้องเป็นสคริปต์แรกในแท็ก <head> ที่โหลดแบบ synchronous จาก CDN ของ Shopify โดยห้ามทำการมัดรวม (bundle) หรือเลื่อนการโหลด (defer) เด็ดขาด
บทความต้นฉบับยังได้ยกตัวอย่างปัญหาชวนปวดหัวในระบบแปลภาษาของ Symfony เมื่อข้อความหัวอีเมลอย่างเช่น digest.attachment : ' Attached: your restock list (%count% items).' ถูกตัดคำสองคำแรกออกอย่างไร้ร่องรอยในระบบจริง เนื่องจาก %count% เป็นตัวเลข ทำให้ข้อความถูกส่งผ่านลอจิกการทำพหูพจน์ของ Symfony และตรงกับเงื่อนไขการตรวจจับข้อความที่มีเครื่องหมายโคลอน ทำให้คำนำหน้าถูกกลืนหายไปโดยไม่มีการบันทึก Log หรือแจ้งข้อผิดพลาดใด ๆ บทเรียนสำคัญจากเหตุการณ์นี้คือ อย่าให้ข้อความที่มีตัวแปร %count% ขึ้นต้นด้วยคำที่มีเครื่องหมายโคลอนตามหลัง และควรเขียนชุดทดสอบที่ตรวจสอบสตริงตั้งแต่ตัวอักษรแรกเสมอ
ที่มา: Dev.to
พบข้อมูลผิดพลาดในบทความนี้? แจ้งปัญหาบทความนี้
ความคิดเห็น
แสดงความคิดเห็น