ยุคใหม่ไร้รหัสผ่าน!! Passkey จะมาแทนที่รหัสผ่านจริงหรือ?
16 ธันวาคม 2025
“รหัสผ่าน” (Password) เป็นแนวคิดที่อยู่กับเรามายาวนานตั้งแต่ยุคโบราณ มันเป็นเหมือนกุญแจด่านแรก (และอาจจะเป็นด่านเดียว แล้วแต่บริบท) ในการเข้าถึงบริการดิจิทัลต่าง ๆ แต่ในทางกลับกัน ก็เป็นจุดอ่อนสำคัญในระบบรักษาความปลอดภัยด้วย หากรหัสผ่านรั่วไหล มีความเสี่ยงสูงขึ้นมากที่ผู้บุกรุกจะสามารถเข้าถึงระบบโดยไม่ได้รับอนุญาตได้
เพื่อแก้ปัญหาตรงนี้ ที่ผ่านมาเราพยายามที่จะเพิ่มความปลอดภัยด้วยการใช้เทคโนโลยีอื่น ๆ ช่วย อย่างเช่น การเปิดใช้งาน Multi-Factor Authentication หรือการใช้งาน Password Manager ซึ่งช่วยป้องกันมากขึ้นได้จริงแต่ก็ยังคงไม่สมบูรณ์ และรหัสผ่านก็ยังคงเป็นส่วนหนึ่งในระบบ ไม่มีอะไรมาแทนที่ได้
คำถามคือ… ถึงเวลาแล้วหรือยังที่เราจะหาอะไรที่ดีกว่ามาแทนที่การใช้รหัสผ่านโดยสมบูรณ์?
บทความนี้จะพาคุณเจาะลึกในฐานะผู้เชี่ยวชาญด้านความปลอดภัยว่า Passkey คืออะไร, ทำไมมันถึงถูกขนานนามว่าจะเป็นผู้ฆ่ารหัสผ่าน, มันป้องกันการโจมตีอย่าง Phishing ได้อย่างไร และในฐานะนักพัฒนาหรือผู้ดูแลระบบ เราควรจะต้องระวังอะไรบ้างในการนำมันมาใช้งาน
1. ปัญหาโลกแตกของ “รหัสผ่าน” ที่เรากำลังเผชิญ
ก่อนที่เราจะไปพูดถึง Passkey เราต้องเข้าใจก่อนว่าทำไมโลกทั้งใบถึงพยายามหนีจากรหัสผ่าน
ภัยคุกคามในยุคดิจิทัลนี้ รหัสผ่านมีปัญหาพื้นฐานอยู่ 3 ประการ:
- ปัญหาด้านความจำ ทำให้การที่ผู้ใช้งานระบบจำเป็นต้องจำรหัสผ่านที่กำหนดอย่างซับซ้อนได้ แต่โดยทั่วไปแล้ว มนุษย์ไม่สามารถจำรหัสผ่านที่ “ดี” (ยาว, ซับซ้อน, ไม่ซ้ำกัน) ของทุกบริการที่ใช้งานไหว ผลลัพธ์คือการใช้รหัสผ่านง่าย ๆ (เช่น123456, password) หรือการใช้รหัสผ่านซ้ำ ๆ ในทุกบริการ (Password Reuse) ซึ่งเป็นเหตุผลที่ทำให้การสุ่มยิง Username/Password ที่ได้จากบริการอื่นได้ผลดี หรือเรียกว่าเทคนิคการโจมตีด้วย Credential Stuffing
- ปัญหาด้านการรั่วไหลข้อมูล (Data Breach) คือ รหัสผ่านยังต้องถูกเก็บไว้ที่ฝั่งเซิร์ฟเวอร์ (แม้ว่าจะเป็น Hash) ทำให้เมื่อเซิร์ฟเวอร์ถูกเจาะ ข้อมูล Hash ของรหัสผ่านก็จะรั่วไหลออกไป ผู้ไม่ประสงค์ดีสามารถนำไปตรวจหารหัสผ่านที่แท้จริงได้ (Password Cracking)
- ปัญหาด้านการหลอกลวงทางไซเบอร์ (Phishing) คือมนุษย์ถูกหลอกได้ นี่คือปัญหาที่ใหญ่ที่สุด เพราะมาตรการความปลอดภัยทุกรูปแบบที่สร้างไว้จะไม่มีความหมายเลยถ้าผู้ใช้งานเป็นคนบอกรหัสผ่านทั้งหมดให้ผู้ไม่ประสงค์ดีเอง ไม่ว่าจะผ่านเว็บไซต์ปลอมหรือการแชร์หน้าจอ
แม้เราจะมี MFA (Multi-Factor Authentication) อย่าง TOTP (ด้วยแอปพลิเคชัน Google Authenticator) หรือ SMS OTP มาช่วยลดความเสี่ยง แต่ก็ไม่ได้ไร้เทียมทาน อย่างเช่นการโจมตีแบบ Adversary-in-the-Middle (AiTM) ที่ใช้เครื่องมืออย่าง evilginx2 เพื่อดักจับทั้ง Username, Password และ OTP ที่ผู้ใช้งานตัวจริงเป็นคนกรอกให้เอง และดักจับ Session Token ที่เซิร์ฟเวอร์ส่งกลับมาไปใช้งานต่อ

รูปจาก https://www.cyfence.com/article/test-enterprise-security-awareness-with-gophish/
2. Passkey คืออะไร และทำงานอย่างไร?
Passkey เป็นข้อมูลรูปแบบใหม่สำหรับใช้ยืนยันตัวตน (Credential) ที่ออกแบบบนมาตรฐาน FIDO2 ซึ่งมีองค์ประกอบสำคัญ 2 ส่วน คือ
- WebAuthn (Web Authentication): เป็น JavaScript API บนเบราว์เซอร์และแพลตฟอร์มที่รองรับ ทำให้ผู้พัฒนาเว็บแอปพลิเคชันสามารถใช้การยืนยันตัวตนแบบ FIDO2 ได้
- CTAP2 (Client to Authenticator Protocol 2): เป็นโปรโตคอลที่ทำให้เครื่องยืนยันตัวตนเคลื่อนที่ได้ (Roaming Authenticator) เช่น โทรศัพท์มือถือ และ YubiKey สามารถสื่อสารกับเบราว์เซอร์หรือแพลตฟอร์มที่รองรับ FIDO2 ได้
ถ้าจะให้อธิบายง่ายที่สุด หลักการของ Passkey คือการนำแนวคิด “การเข้ารหัสแบบกุญแจสาธารณะ” (Public-Key Cryptography) มาแทนที่ “ความลับที่ใช้ร่วมกัน” (Shared Secret) อย่างรหัสผ่าน
กระบวนการทำงานของ Passkey แบ่งเป็น 2 ส่วน:
ส่วนที่ 1: การลงทะเบียน (Registration)
- User: ผู้ใช้งานต้องการสร้าง Passkey กับเว็บไซต์ mybank.com
- Relying Party (Server): เซิร์ฟเวอร์ mybank.com ส่งรูปแบบ Passkey ที่เซิร์ฟเวอร์รองรับให้กับเบราว์เซอร์ พร้อมกับ Challenge ที่เป็นค่าสุ่ม
- Browser (Platform): เบราว์เซอร์นำข้อมูลที่ได้รับไปเรียกใช้งาน WebAuthn API บนเบราว์เซอร์เพื่อสื่อสารกับอุปกรณ์ยืนยันตัวตนของผู้ใช้งาน
- Authenticator (Device): อุปกรณ์ของผู้ใช้งาน (เช่น iPhone หรือ Macbook) จะ:
+ ให้ผู้ใช้งานยืนยันตัวตนถ้าจำเป็น (เช่น สแกนใบหน้า, ลายนิ้วมือ หรือใส่ PIN ของอุปกรณ์) เพื่อยืนยันว่าเจ้าของอุปกรณ์เป็นผู้เริ่มกระบวนการ
+ สร้าง “คู่กุญแจ” (Key Pair) ใหม่ 1 คู่ คือ Private Key (กุญแจส่วนตัว) และ Public Key (กุญแจสาธารณะ)
+ เก็บ Private Key ไว้ในที่ปลอดภัยสูงสุด (เช่น Secure Enclave, TPM) ซึ่ง Private Key นี้จะไม่มีวันส่งออกจากอุปกรณ์เด็ดขาด*
+ ส่ง Public Key และ Challenge กลับไปให้เบราว์เซอร์ - Browser (Platform): เบราว์เซอร์ส่งข้อมูลจากอุปกรณ์ของผู้ใช้งานไปที่เซิร์ฟเวอร์ mybank.com
- Relying Party (Server): เซิร์ฟเวอร์ mybank.com ตรวจสอบข้อมูลและเก็บ Public Key นี้ไว้ โดยผูกกับบัญชีของผู้ใช้งาน เพื่อใช้ตรวจสอบ Signature ในอนาคต
* หมายเหตุ: ปัจจุบัน เริ่มมีการใช้งาน Synced Passkey ที่สามารถสำรองข้อมูล Passkey หนึ่ง ๆ บนหลาย Authenticator ได้อย่างปลอดภัย ทำให้เกิดเป็นกรณีเดียวที่มีสามารถเคลื่อนย้าย Private Key ออกจากเครื่อง Authenticator แต่ทั้งนี้ ตามหลักการดั้งเดิมแล้ว Private Key จะไม่ควรส่งออกจากอุปกรณ์เด็ดขาดเพื่อความปลอดภัย
ส่วนที่ 2: การยืนยันตัวตน (Authentication)
- User: ผู้ใช้งานต้องการเข้าสู่ระบบบนเว็บไซต์ mybank.com
- Relying Party (Server): เซิร์ฟเวอร์ส่ง Challenge ใหม่มาให้
- Browser (Platform): เบราว์เซอร์นำข้อมูลที่ได้รับไปเรียกใช้งาน WebAuthn API บนเบราว์เซอร์เพื่อสื่อสารกับอุปกรณ์ยืนยันตัวตนของผู้ใช้งาน
- Authenticator (Device): อุปกรณ์ของผู้ใช้งานจะ:
+ ให้ผู้ใช้งานยืนยันตัวตนถ้าจำเป็น (เช่น สแกนใบหน้า, ลายนิ้วมือ หรือใส่ PIN ของอุปกรณ์) เพื่อ “ปลดล็อก” การใช้งาน Private Key
+ เลือก Private Key ที่เคยสร้างไว้สำหรับเข้าใช้งานเว็บไซต์ mybank.com หากมีหลายอัน
+ ใช้ Private Key ที่เลือก “เซ็น” ข้อมูลต่าง ๆ รวมถึง Challenge ใหม่
+ ส่ง Signature พร้อมข้อมูลต่าง ๆ กลับไปให้เบราว์เซอร์ - Browser (Platform): เบราว์เซอร์ส่งข้อมูลจากอุปกรณ์ของผู้ใช้งานไปที่เซิร์ฟเวอร์ mybank.com
- Relying Party (Server): เซิร์ฟเวอร์นำ Signature นี้ มาตรวจสอบกับ Public Key ที่เก็บไว้ ถ้าถูกต้อง ก็ถือว่ายืนยันตัวตนสำเร็จ
3. ทำไม Passkey ถึงดีกว่า Password?
จากหลักการทำงานข้างต้น เราจะเห็นว่าการออกแบบของ Passkey ช่วยแก้ปัญหาของรหัสผ่านที่เกริ่นไปก่อนหน้าได้ทุกข้อ:
- แก้ปัญหาการจำ: ผู้ใช้งานไม่ต้องจำอะไรเพิ่มเติมในการเข้าใช้บริการเลย นอกจาก PIN หรือการสแกนใบหน้า/ลายนิ้วมือที่ใช้กับอุปกรณ์ของตัวเองอยู่แล้ว
- แก้ปัญหา Credential Stuffing: การโจมตีนี้จะหายไปโดยสมบูรณ์ เพราะไม่มี “รหัสผ่าน” ส่งออกมาจากเครื่องผู้ใช้งานงานให้เอาไปสุ่มยิงที่อื่น และ Private Key ก็สุ่มใหม่ทุกครั้งที่ลงทะเบียนกับบริการต่าง ๆ
- แก้ปัญหาการรั่วไหลข้อมูล: สิ่งที่เซิร์ฟเวอร์เก็บคือ Public Key ซึ่งเป็นข้อมูล “สาธารณะ” ต่อให้รั่วไหลไป Attacker ก็ไม่สามารถนำไปทำอะไรได้เลย เพราะไม่มี Private Key
- แก้ปัญหาด้านการหลอกลวงทางไซเบอร์: Passkey เป็นรูปแบบข้อมูลที่ป้องกันการ Phishing ได้ในตัวเอง (Phishing-Resistant) ซึ่งเป็นจุดแข็งที่สุด และเราจะเจาะลึกเรื่องนี้ในหัวข้อถัดไป
การที่ Passkey สามารถแก้ปัญหาทั้งหมดนี้ได้ โดยที่ทำให้การยืนยันตัวตนสะดวกสบายขึ้นพร้อมกัน จึงเป็นเหตุผลที่ Passkey ได้รับการขนานนามว่าเป็นสิ่งที่จะมา “ฆ่ารหัสผ่าน” นั่นเองครับ

4. กลไกการป้องกัน Phishing อย่างแท้จริงของ Passkey
Passkey สามารถป้องกัน Phishing ได้โดยสมบูรณ์ วิธีการคือตอนที่สร้าง Key Pair จะมีการผูก Key Pair นั้น ๆ เข้ากับค่า Relying Party ID (RP ID) ที่มักจะมีค่าเป็น Domain ของแอปพลิเคชัน และ Origin ซึ่งมักจะตรงกับ Base URL ของเว็บไซต์ เพื่อป้องกันไม่ให้นำ Key Pair นั้น ๆ ไปใช้งานกับแอปพลิเคชันอื่นที่มี Domain ไม่ตรงกับค่าที่ตั้งไว้
เพื่อให้เห็นภาพ เรามาลองดูการโจมตีแบบ Adversary-in-the-Middle (AiTM) กันครับ เป็นการโจมตีที่ผู้ไม่ประสงค์ดีจะคุยกับเว็บไซต์จริง แล้วส่งต่อข้อมูลที่ได้ไปที่เว็บไซต์ปลอม เพื่อหลอกเหยื่อที่กำลังใช้งานเว็บไซต์ปลอมอยู่ว่ากำลังคุยกับเว็บไซต์จริงครับ
เรามาลองดูสถานการณ์ตัวอย่าง 2 แบบที่จะเกิดขึ้น เมื่อผู้ไม่ประสงค์ดีพยายามโจมตีแบบ AiTM กับเว็บไซต์ที่ใช้ Password + MFA (TOTP) และเว็บไซต์ที่ใช้ Passkey ครับ
สถานการณ์ที่ 1: Password + MFA (TOTP)
- ผู้ไม่ประสงค์ดี: สร้างเว็บ Phishing ชื่อ mybank-login.com (Domain เว็บจริง ๆ คือ mybank.com) และใช้ evilginx2 เป็น Proxy
- เหยื่อ: ถูกหลอกให้เข้า mybank-login.com และกรอก Username + Password
- ผู้ไม่ประสงค์ดี: (Proxy) ส่งข้อมูลนี้ไป mybank.com (สำเร็จ) และขอ TOTP
- เหยื่อ: ผู้ใช้งานกรอก TOTP จากแอปพลิเคชัน Authenticator ของตนผ่าน mybank-login.com
- ผู้ไม่ประสงค์ดี: (Proxy) ส่ง TOTP ไปที่ mybank.com (สำเร็จ)
- ผลลัพธ์: ผู้ไม่ประสงค์ดีได้รับ Session Cookie และเข้ายึดบัญชีของเหยื่อได้สำเร็จ
สถานการณ์ที่ 2: Passkey
- ผู้ไม่ประสงค์ดี: สร้างเว็บ Phishing ชื่อ mybank-login.com (Domain เว็บจริง ๆ คือ mybank.com) และใช้ evilginx2 เป็น Proxy
- เหยื่อ: ถูกหลอกให้เข้า mybank-login.com และกดปุ่ม Login
- ผู้ไม่ประสงค์ดี: (Proxy) เริ่มกระบวนการยืนยันตัวตนด้วย Passkey กับ mybank.com แล้วส่งข้อมูลต่อให้ผู้ใช้งาน
- เหยื่อ: เบราว์เซอร์ของผู้ใช้งานได้รับคำสั่งให้ “ค้นหา” Passkey ที่จะใช้กับ mybank-login.com
- จุดสำคัญ: ตอนที่ลงทะเบียน Passkey อุปกรณ์ของผู้ใช้งานได้บันทึกไว้ว่า “Private Key นี้ สร้างขึ้นสำหรับ RP ID mybank.com เท่านั้น”
- ผลลัพธ์: เมื่อเบราว์เซอร์ทำงานบนโดเมน mybank-login.com อุปกรณ์จะมองหา Passkey ที่ผูกกับ mybank-login.com เท่านั้น ซึ่ง “ไม่พบ” มันจึง ไม่เสนอ Passkey ของ mybank.com ให้ผู้ใช้งานเลือก และเมื่อ
- ไม่มี Passkey ที่ใช้งานได้ อุปกรณ์ก็จะปฏิเสธการเข้าสู่ระบบโดยอัตโนมัติ
- ผู้ไม่ประสงค์ดี: การโจมตีล้มเหลว โดยที่ผู้ใช้งานยังไม่ทันได้ทำอะไรผิดแปลกจากกระบวนการปกติเลย
จุดนี้คือสิ่งที่ทำให้ Passkey เหนือกว่า MFA แบบ TOTP อย่างชัดเจน เพราะการ Phishing โดยทั่วไปอาจจะสามารถหลอกผู้ใช้งานได้ แต่ไม่สามารถหลอกการทำงานตามตรรกะของ Passkey ได้ และต่อให้ผู้ไม่ประสงค์ดีจะพยายามหลอกเหยื่อยังไง ก็ไม่สามารถรู้ “รหัสผ่าน” ที่แท้จริงอย่าง Private Key ที่เก็บอยู่ในอุปกรณ์ของผู้ใช้งานได้

5. Passkey ในมุมมองของมาตรฐานสากลโดย NIST & OWASP
ในฐานะผู้ตรวจสอบและที่ปรึกษาด้านความปลอดภัย เราควรจะอ้างอิงมาตรฐานสากลเสมอ ซึ่งในบทความนี้ ก็จะกล่าวถึงมาตรฐานของ NIST และ OWASP ครับ
ในเอกสาร NIST SP 800-63 ได้กำหนดระดับความน่าเชื่อถือของการยืนยันตัวตน (Authentication Assurance Levels – AAL) เป็น 3 ระดับ ดังนี้
- AAL1: วิธีการยืนยันตัวตนพื้นฐาน มีความเสี่ยงสูงต่อการโจมตีทั่วไป เช่น Password
- AAL2: วิธีการยืนยันตัวตนความปลอดภัยสูง สามารถป้องกันการโจมตีทั่วไปได้ด้วยการยืนยันตัวตนหลายปัจจัย เช่น MFA
- AAL3: วิธีการยืนยันตัวตนความปลอดภัยสูงสุด สามารถป้องกันการปลอมแปลงอุปกรณ์ที่ใช้ยืนยันตัวตน และตรวจจับการ Compromise ของอุปกรณ์ที่ใช้ยืนยันตัวตนได้ ซึ่งมักจะเป็นอุปกรณ์ฮาร์ดแวร์ที่รองรับการยืนยันตัวตนหลายปัจจัย
Synced Passkey อย่างเช่น iCloud Keychain ได้รับการรับรองโดย NIST ในระดับ AAL2 ซึ่งแสดงถึงความปลอดภัยและความน่าเชื่อถือของการยืนยันตัวตนรูปแบบนี้ ในขณะที่ Device-bound Passkey หรือ Passkey ที่ผูกอยู่กับอุปกรณ์ยืนยันตัวตนอย่างถาวรอย่าง YubiKey จะจัดอยู่ใน AAL3 ครับ
ในขณะที่ OWASP ASVS 5.0.0 ซึ่งเป็นรุ่นล่าสุด มาตรฐานการยืนยันตัวตนจะอยู่ในหัวข้อ V6 Authentication ซึ่งก็มีความเห็นสอดคล้องกับแนวคิดของ NIST เช่นกัน โดยระบุว่าแอปพลิเคชันที่พุ่งเป้าไปที่ระดับความปลอดภัย L3 ควรจะใช้เทคโนโลยีการยืนยันตัวตนที่อยู่ในระดับ AAL3 ซึ่งรวมถึง Device-bound Passkey ด้วยครับ
ตัวอย่างบางส่วนจากบทความจาก OWASP ASVA version 5.0.0
“L2 applications must force the use of multi‑factor authentication (MFA). L3 applications must use hardware‑based authentication, performed in an attested and trusted execution environment (TEE). This could include device‑bound Passkey, eIDAS Level of Assurance (LoA) High enforced authenticators, authenticators with NIST Authenticator Assurance Level 3 (AAL3) assurance, or an equivalent mechanism.”
หากสนใจว่า OWASP ASVS มีรายละเอียดอย่างไรบ้าง ทางเรามีบทความเกี่ยวกับเรื่องนี้โดยเฉพาะ สามารถเข้าไปอ่านได้ที่บทความด้านล่างนี้เลยครับ
URL: https://www.cyfence.com/article/owasp-asvs/
จึงสรุปได้ว่า การเปลี่ยนไปใช้ Passkey ในการยืนยันตัวตนนั่นเป็นการเพิ่มความปลอดภัยของระบบเป็นอย่างมากครับ
6. แนวทางการ Implement Passkey
หนึ่งในวิธีการยอดนิยมคือการ Implement ด้วย Library ที่เชื่อถือได้ อย่าพยายามเขียนโค้ด WebAuthn เองจาก 0 เนื่องจากรายละเอียดมีความซับซ้อนหลายส่วน เช่น การจัดการ CBOR, COSE, Attestation หากเลือกเส้นทางนี้ ทาง ผู้เขียน ก็ขอแนะนำให้ใช้ Library ที่ผ่านการตรวจสอบแล้วครับ เช่น:
- Node.js/TypeScript: simplewebauthn (มีทั้งฝั่งเซิร์ฟเวอร์และเบราว์เซอร์)
- Python: py_webauthn
- Go: WebAuthn Go library
- Java: WebAuthn4J
ทั้งนี้ มีคนรวบรวม Passkey Library มาแล้วใน Github Repository ด้านล่างนี้ครับ
URL: https://github.com/yackermann/awesome-webauthn
เพื่อให้เห็นภาพการ Implement ที่ถูกต้อง (และมีช่องโหว่ให้เรียนรู้) พวกเรา ผู้เขียน ได้สร้างโปรเจกต์ตัวอย่างชื่อว่า Damn Vulnerable Passkey (DVP) ขึ้นมาครับ
URL: https://github.com/siamthanathack/Damn-Vulnerable-Passkey

ซึ่งโปรเจกต์นี้มีการเลือก Implement ด้วยเทคโนโลยีดังนี้ครับ
- Database: sqlite3
- Server: Python Flask และ py_webauthn (https://github.com/duo-labs/py_webauthn)
- Website: @simplewebauthn/browser (https://github.com/MasterKale/SimpleWebAuthn/tree/master/packages/browser)
หลังจากติดตั้งทุกอย่างตามในไฟล์ README.md เราจะสามารถเข้าไปที่เว็บแอปพลิเคชันได้ที่ URL นี้ได้เลยครับ
URL: http://localhost:5000

ก่อนอื่น เรามาดูการ Implement ที่ปลอดภัยของ Lab 0 จะเห็นตัวอย่างการตั้งค่า Policy ที่เข้มงวด

สำหรับบทความนี้ เราจะมาลองดูวิธีการ Implement Passkey ผ่านโค้ดของแอปพลิเคชันนี้ครับ
บนหน้าเว็บนี้ จะมีการเรียก API ทั้งหมด 4 เส้นครับ คือ
- /lab0/registration_start เป็น API สำหรับเริ่มต้นการลงทะเบียน
- /lab0/registration_verify เป็น API สำหรับส่งข้อมูลจาก Authenticator เพื่อลงทะเบียน
- /lab0/authentication_start เป็น API สำหรับเริ่มต้นการยืนยันตัวตน
- /lab0/authentication_verify เป็น API สำหรับส่งข้อมูลจาก Authenticator เพื่อยืนยันตัวตน
ที่ปุ่ม Register จะมีหน้าที่หลัก ๆ เพียงแค่เรียก API เพื่อเริ่มกระบวนการลงทะเบียน รับข้อมูลมาส่งต่อให้ Authenticator และส่งข้อมูลจาก Authenticator ไปที่เซิร์ฟเวอร์เท่านั้นครับ
ซึ่งจะสังเกตได้ว่า เราแทบไม่จำเป็นต้องเขียน Logic อะไรเพิ่มเติมเลย เพราะ WebAuthN API จัดการให้เราหมดแล้ว
async function register() {
const username = document.getElementById("username").value.trim();
if (username) {
# เรียก API /lab0/registration_start เพื่อเริ่มต้นกระบวนการ
const apiRegOptsResp = await fetch('/lab0/registration_start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username,
user_verification: 'preferred',
attestation: 'none',
attachment: 'platform',
algorithms: ['es256', 'rs256'],
discoverable_credential: 'preferred'
})
});
if (apiRegOptsResp.status != 200) {
alert(await apiRegOptsResp.text());
return;
}
const registrationOptionsJSON = await apiRegOptsResp.json();
# เรียกใช้ WebAuthN API ผ่านคำสั่งใน Library @simplewebauthn/browser เพื่อสร้าง Passkey ใหม่ตามที่เซิร์ฟเวอร์รองรับ
const regResp = await startRegistration(registrationOptionsJSON);
# ส่งข้อมูลจาก Authenticator ไปที่ API /lab0/registration_verify เพื่อจบกระบวนการ
const apiRegVerResp = await fetch('/lab0/registration_verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username,
response: regResp,
}),
});
alert(await apiRegVerResp.text())
} else {
alert("Please enter a valid username.");
}
}
ทีนี้ เราเปลี่ยนมาดูที่ API /lab0/registration_start เส้นแรกครับ ซึ่งมีหน้าที่กำหนดรูปแบบ Passkey ที่เซิร์ฟเวอร์รองรับแล้วส่งกลับไปที่เบราว์เซอร์ครับ
@lab0_bp.route('/lab0/registration_start', methods=['POST'])
def registrationStart():
username = request.json['username']
# ตรวจสอบว่าบัญชีผู้ใช้งานมี Passkey ในระบบหรือไม่ (ไม่ต้องมีส่วนนี้ก็ได้)
existing_pubkeys = get_available_public_key_by_username(username)
# เซิร์ฟเวอร์กำหนดรูปแบบ Passkey ที่รองรับด้วย Library py_webauthn
resp = generate_registration_options(
rp_id = os.getenv("RP_ID"),
rp_name = os.getenv("RP_NAME"),
user_name = username,
user_id = secrets.token_bytes(32),
exclude_credentials = [
PublicKeyCredentialDescriptor(
id = pubkey
) for pubkey in existing_pubkeys
]
)
# เก็บ Challenge ไว้เพื่อตรวจสอบทีหลัง
store_challenge(username, resp.challenge)
return Response(options_to_json(resp), content_type='application/json')
หลังจากนั้น ฝั่งเบราว์เซอร์ที่รับข้อมูลมา ก็จะนำข้อมูลส่งต่อให้ Authenticator ประมวลผลต่อ และส่งกลับมาที่ API /lab0/registration_verify
ในความเป็นจริง จะมีการตรวจสอบข้อมูลต่อไปนี้
- ตรวจสอบ Challenge ของผู้ใช้งานนี้ว่าตรงกับตอนเริ่มกระบวนการหรือไม่
- ตรวจสอบ Origin และ RP_ID (Relying Party ID) ว่าตรงกับค่าของแอปพลิเคชันหรือไม่
- ตรวจสอบความน่าเชื่อถือของ Authenticator ที่ใช้ยืนยันตัวตน
- ตรวจสอบ UV Flag และค่าอื่น ๆ ว่าตรงตามที่การตั้งค่าที่เซิร์ฟเวอร์รองรับหรือไม่
ซึ่งทั้งหมดนี้ Library py_webauthn จัดการให้เราหมดแล้วผ่านฟังก์ชัน verify_registration_response ครับ
@lab0_bp.route('/lab0/registration_verify', methods=['POST'])
def registrationVerify():
username = request.json['username']
credential = parse_registration_credential_json(request.json['response'])
expected_challenge = get_challenge(username)
# เรียกใช้งานคำสั่งของ Library py_webauthn เพื่อตรวจสอบข้อมูลจากผู้ใช้งาน
resp = verify_registration_response(
credential = credential,
expected_challenge = expected_challenge,
expected_rp_id = os.getenv("RP_ID"),
expected_origin = os.getenv("ORIGIN")
)
# นำ Public Key ของผู้ใช้งานไปเก็บในฐานข้อมูล
store_pubkey(username, resp.credential_id, resp.credential_public_key)
# ลบ Challenge ที่ใช้แล้วออก ป้องกันการใช้ซ้ำ
delete_challenge(username)
return "Registration Completed"
เท่านี้ เราก็สามารถลงทะเบียน Passkey ได้แล้วครับ

หลังจากนี้ ก็จะเป็นในส่วนของการยืนยันตัวตนครับ
ที่ปุ่ม Authenticate ก็จะมีโค้ดคล้าย ๆ กับปุ่ม Register ครับ แค่เปลี่ยน API ที่เรียกเป็น API สำหรับการยืนยันตัวตนนั่นเอง
async function authenticate() {
const username = document.getElementById("username").value.trim();
if (username) {
# เรียก API /lab0/authentication_start เพื่อเริ่มต้นกระบวนการ
const apiAuthOptsResp = await fetch('/lab0/authentication_start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username
})
});
if (apiAuthOptsResp.status != 200) {
alert(await apiAuthOptsResp.text());
return;
}
const authenticationOptionsJSON = await apiAuthOptsResp.json();
# เรียกใช้ WebAuthN API ผ่านคำสั่งใน Library @simplewebauthn/browser เพื่อเรียกใช้ Passkey ของผู้ใช้งาน
const authResp = await startAuthentication(authenticationOptionsJSON);
# ส่งข้อมูลจาก Authenticator ไปที่ API /lab0/authentication_verify เพื่อจบกระบวนการ
const apiAuthVerResp = await fetch('/lab0/authentication_verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username,
response: authResp,
}),
});
alert(await apiAuthVerResp.text())
} else {
alert("Please enter a valid username.");
}
}
API /lab0/authentication_start จะทำหน้าที่กำหนด Passkey ที่สามารถใช้ยืนยันตัวตนได้นั่นเองครับ คล้ายกับ API /lab0/registration_start นั่นเอง
@lab0_bp.route('/lab0/authentication_start', methods=['POST'])
def authenticationStart():
username = request.json['username']
# ตรวจสอบว่าบัญชีผู้ใช้งานมี Passkey ในระบบหรือไม่ (ไม่ต้องมีส่วนนี้ก็ได้)
existing_pubkeys = get_available_public_key_by_username(username)
if not existing_pubkeys:
return "This username does not have a Passkey registered!", 404
# เซิร์ฟเวอร์กำหนดรูปแบบ Passkey ที่รองรับด้วย Library py_webauthn
resp = generate_authentication_options(
rp_id = os.getenv("RP_ID"),
allow_credentials = [
PublicKeyCredentialDescriptor(
id = pubkey
) for pubkey in existing_pubkeys
]
)
# เก็บ Challenge ไว้ในฐานข้อมูลเพื่อตรวจสอบทีหลัง
store_challenge(username, resp.challenge)
return Response(options_to_json(resp), content_type='application/json')
เมื่อผู้ใช้งานยืนยันตัวตนเสร็จแล้ว จะส่งข้อมูลมาที่ API /lab0/authentication_verify ซึ่ง API จะต้องตรวจสอบ Signature ว่าถูกเซ็นมาด้วย Passkey ที่ถูกต้องหรือไม่
@lab0_bp.route('/lab0/authentication_verify', methods=['POST'])
def authenticationVerify():
username = request.json['username']
credential = parse_authentication_credential_json(request.json['response'])
expected_challenge = get_challenge(username)
credential_public_key, credential_current_sign_count = get_pubkey_and_counter(username, credential.raw_id)
# ตรวจสอบ Signature ที่ผู้ใช้งานส่งมาด้วย Library py_webauthn
resp = verify_authentication_response(
credential = credential,
expected_challenge = expected_challenge,
expected_rp_id = os.getenv("RP_ID"),
expected_origin = os.getenv("ORIGIN"),
credential_public_key = credential_public_key,
credential_current_sign_count = credential_current_sign_count
)
# ลบ Challenge ที่ใช้แล้วออก ป้องกันการใช้ซ้ำ
delete_challenge(username)
# ยืนยันตัวตนผู้ใช้งานด้วยการส่ง Session ให้ผู้ใช้งาน
return login(username)
เท่านี้ เราก็จะ Implement การยืนยันตัวตนผ่าน Passkey ได้สำเร็จครับ

เราจะเห็นได้ว่าการใช้ Library ที่น่าเชื่อถือ ช่วยทุ่นแรงลงไปได้มาก โดยเฉพาะโค้ดที่หากเขียนเองแล้วมีสิทธิที่จะผิดพลาดสูง แต่ก็ยังคงจำเป็นต้องมีส่วนที่พัฒนาเองอยู่ เช่น การจัดการฐานข้อมูลเพื่อจัดเก็บ Challenge และ Public Key ของบัญชีผู้ใช้งานต่าง ๆ
7. ข้อควรระวังในการ Implement Passkey
ถึงแม้ว่า Library ที่น่าเชื่อถือจะช่วยแบ่งเบาภาระไปมากแล้ว แต่นั้นก็ไม่ได้หมายความว่าเราจะ Implement Passkey โดยไม่รู้อะไรเลยได้ เพราะถ้ามีการตั้งค่าผิด บางการตั้งค่าก็ทำให้ระบบใช้งานไม่ได้ บางการตั้งค่าก็ทำให้เกิดช่องโหว่ได้เลย
เพื่อให้เห็นภาพมากขึ้น เรามาลองดูตัวอย่างที่ค่าต่อไปนี้กันดีกว่าครับ
Relying Party ID (RP ID) และ Origin
ค่าเหล่านี้เกี่ยวข้องกับเรื่องการป้องกัน Phishing ซึ่งปกติ RP ID จะตรงกับโดเมนของแอปพลิเคชัน และ Origin จะตรงกับ Base URL ของแอปพลิเคชัน
ตัวแปรเหล่านี้ ถ้าตั้งค่าไม่ถูกต้อง บาง Library จะตรวจสอบให้อยู่แล้ว โดยจะยกเลิกกระบวนการ Passkey ทันทีที่ตรวจพบ แปลว่าแอปพลิเคชันจะไม่สามารถใช้งานได้เลย

ทว่า ในบางกรณีก็สามารถตั้งค่าเป็น Wildcard ซึ่งออกแบบให้ทำได้ในระดับ Subdomain (*.domain.com) เพื่อให้สามารถใช้งาน Passkey เดียวกันบนหลายเว็บแอปพลิเคชันที่ผู้ให้บริการเป็นคนเดียวกันได้
Wildcard นั้นหากใช้ไม่ระวัง ก็อาจจะเปิดโอกาสให้ผู้ไม่ประสงค์ดีสามารถเข้าถึงระบบที่ไม่ควรเข้าถึงได้ อย่างเช่น การนำ Passkey ที่ลงทะเบียนกับ user.subdomain.com ไปใช้เข้าสู่ระบบบนแอปพลิเคชัน admin.subdomain.com ที่ใช้สถาปัตยกรรมภายในเดียวกันในการจัดการผู้ใช้งานครับ
User Verification (UV)
ค่า User Verification (UV) เป็นค่าที่ระบุว่า Authenticator ควรจะยืนยันตัวตนผู้เริ่มกระบวนการ Passkey ว่าเป็นเจ้าของ Authenticator จริงหรือไม่ ซึ่งมักจะเป็นวิธีการสแกนใบหน้า/ลายนิ้วมือ หรือใส่ PIN ก่อนที่ Private Key จะสามารถใช้งานได้

ค่านี้กำหนดที่เซิร์ฟเวอร์ได้ 3 แบบ คือ required, preferred หรือ discouraged ซึ่งไล่ตั้งแต่ปลอดภัยที่สุด (ต้องยืนยันตัวตนเสมอ) จนถึงปลอดภัยน้อยที่สุด (ไม่แนะนำให้ยืนยันตัวตน)
ทั้งนี้ Authenticator บางชนิด เช่น เครื่อง Macbook ก็ไม่ได้เชื่อถือค่านี้ แต่ถ้าหาก Authenticator เชื่อถือการตั้งค่าที่ไม่ปลอดภัย ก็จะข้ามการยืนยันตัวตนผู้ใช้ไปเลยนั่นเองครับ เปิดโอกาสให้ผู้อื่นที่ไม่ได้เป็นเจ้าของสามารถใช้งาน Passkey ของแอปพลิเคชันดังกล่าวได้
Challenge
ค่า Challenge ควรเป็นค่าที่สุ่มอย่างคาดเดาไม่ได้ แต่ถ้า Challenge เป็นค่าคงที่ จะเกิดอะไรขึ้นบ้าง
[…]# ตอนเริ่มกระบวนการยืนยันตัวตนchallenge = “1234567890”
[…]# ตอนตรวจสอบข้อมูล
expected_challenge = expected_challenge
[…]
ผลที่ตามมา คือ ผลลัพธ์การเข้ารหัสโดย Private Key เดียวกัน จะได้ผลลัพธ์เดียวกันเสมอครับ
- Signature ครั้งที่ 1: morTgy[…]YdHP6D
- Signature ครั้งที่ 2: morTgy[…]YdHP6D
- Signature ครั้งที่ 3: morTgy[…]YdHP6D
- Signature ครั้งที่ 100: morTgy[…]YdHP6D
ทำให้สามารถนำ Signature ที่เคยใช้ยืนยันไปแล้ว ไปใช้ยืนยันตัวตนทุกครั้งได้เลยครับ
นอกจากการตั้งค่าเหล่านี้ การนำ Passkey มาใช้ ก็อาจจะต้องแก้การดีไซน์ระบบในส่วนอื่น ๆ และอาจจะสร้างปัญหาตามมาด้วย อย่างเช่น ระบบการจัดเก็บ Passkey ที่ลงทะเบียนแล้ว หรือการกู้คืนบัญชีผู้ใช้งานในกรณีที่ Authenticator สูญหาย ซึ่งหากปรับใช้งานได้ไม่ดี ก็อาจจะทำให้ผู้ไม่ประสงค์ดีเข้าถึงบัญชีผู้ใช้งานของผู้อื่นโดยไม่ได้รับอนุญาตได้
สำหรับใครที่สนใจในส่วนนี้ ใน DVP นอกเหนือจาก Lab 0 ซึ่งเป็นตัวอย่างการ Implement ที่ปลอดภัยแล้ว ยังมีเว็บแอปพลิเคชันที่ “จงใจ” Implement Passkey แบบมีช่องโหว่อีกด้วยครับ (คล้าย ๆ DVWA) ซึ่งผมก็ ขอแนะนำว่าให้ลองเข้าไปเล่นดู ถ้าอยากทดสอบความรู้ตัวเองหรือศึกษา Passkey อย่างลึกซึ้ง

นอกเหนือจากนี้ เรายังมีเครื่องมือสำหรับศึกษาและทดสอบ Passkey มานำเสนอด้วยครับ
8. Passkey Raider เครื่องมือคู่ใจการเจาะระบบ Passkey
ในตอนที่ ผู้เขียน เริ่มศึกษา Passkey ก็พบปัญหามากมายเช่น การควบคุมค่าตัวแปรต่าง ๆ ที่ถูกเข้ารหัสโดย Passkey หรืออย่างข้อมูลบางส่วนที่ส่งในรูปแบบ CBOR ซึ่งเป็นกำแพงหินให้ Pentester ทั่วไปไม่สามารถทดสอบความปลอดภัยของระบบ Passkey ได้ง่ายครับ
ด้วยเหตุนี้ ผู้เขียน จึงพัฒนา Passkey Raider ซึ่งเป็นเครื่องมือที่ “ต้องมี” สำหรับ Pentester ที่ต้องการทดสอบ Passkey เป็น Burp Suite Extension ทำหน้าที่ช่วยดักจับ, ถอดรหัส, และ “แก้ไข” Message ของ WebAuthn ที่วิ่งผ่าน Burp Suite ทำให้เราสามารถทดสอบ Attack Scenario ต่าง ๆ ได้ง่ายขึ้น เช่น ลองแก้ Flag UV เป็น false หรือลองแก้ไขข้อมูลบางอย่างในโครงสร้างข้อมูล CBOR ได้ครับ
หากสนใจ สามารถลองศึกษาต่อได้ที่ URL ด้านล่างนี้ได้เลยครับ
URL: https://github.com/siamthanathack/Passkey-Raider

9. สรุปแล้ว Passkey คืออนาคตของการยืนยันตัวตน… ใช่ไหม
ในปัจจุบัน Passkey ไม่ใช่เป็นแค่ไอเดียลอย ๆ หรือเทรนด์ที่ฮิตขึ้นมาชั่วครั้งชั่วคราว แต่มันคือ “วิวัฒนาการ” ที่แท้จริงของการยืนยันตัวตน สามารถแก้ปัญหาที่รากเหง้าของรหัสผ่านได้อย่างแท้จริง กำลังได้รับการผลักดันจากบริษัทเทคยักษ์ใหญ่และรองรับโดยมาตรฐานสากล
การสนับสนุนอย่างล้นหลามเป็นอีกหนึ่งเครื่องยืนยันถึง “อนาคตที่ไร้รหัสผ่าน” ที่อาจจะมาถึงสักวันหนึ่ง แต่การเปลี่ยนผ่านนี้ ก็ไม่ได้หมายความว่าช่องโหว่ในฟังก์ชันการยืนยันตัวตนจะสูญพันธุ์ไปจากโลกนี้ ผู้พัฒนาก็ยังคงต้องมีสติในการพัฒนาและเลือกใช้งานเทคโนโลยี เขียนโค้ดอย่างปลอดภัย แก้ไขบัคและปิดช่องโหว่อย่างทันท่วงทีเหมือนเดิม
สุดท้ายนี้ ทางผู้เขียนก็ขอแนะนำดังนี้ครับ
- สำหรับผู้ใช้งานทั่วไป: เริ่มใช้งาน Passkey กับบริการที่รองรับ (Google, Apple, ฯลฯ)
- สำหรับนักพัฒนา: เริ่มศึกษาและวางแผน Implement Passkey ไม่ว่าจะด้วยการใช้ Library ที่เชื่อถือได้ หรือการ Implement เองจากศูนย์
- สำหรับผู้บริหารและ IT Manager: นี่คือเทคโนโลยีที่จะช่วยลดความเสี่ยงจากการโจมตี Phishing ที่พนักงานของคุณต้องเจอในทุกวันนี้ได้อย่างมหาศาล ถ้าต้องการใช้งาน ควรเริ่มวางแผน Roadmap ในการนำมาปรับใช้ในองค์กรครับ
หวังว่าบทความนี้จะเป็นประโยชน์ และช่วยให้ทุกท่านเข้าใจ Passkey ในมุมมองเชิงลึกด้านความปลอดภัยมากขึ้นครับ
บทความที่เกี่ยวข้อง




