เลิกใช้ JWT
(gist.github.com/samsch)- JWT ไม่เหมาะสำหรับการคงสถานะการล็อกอินของผู้ใช้ และสำหรับจุดประสงค์นี้ คุกกี้เซสชัน แบบปกติเหมาะสมกว่า
- สเปกของ JWT ตั้งอยู่บนสมมติฐานว่าเป็น โทเค็นอายุสั้น ประมาณไม่เกิน 5 นาที ขณะที่เซสชันจำเป็นต้องมีอายุยาวกว่านั้น
- การทำ การยืนยันตัวตนแบบไร้สถานะ ที่ปลอดภัยนั้นทำได้ยาก และหากต้องการจัดการโทเค็นอย่างปลอดภัย สุดท้ายก็ยังต้องมีที่เก็บสถานะบางส่วนอยู่ดี
- JWT ที่เก็บเพียงโทเค็นเซสชันอย่างเดียวนั้นไม่มีประสิทธิภาพและยืดหยุ่นน้อยกว่าคุกกี้เซสชันทั่วไป และไม่ควรเก็บข้อมูลยืนยันตัวตนไว้ใน localStorage หรือ sessionStorage
- หากต้องการโทเค็นแบบเซ็นลายเซ็นที่มีอายุสั้นจริง ๆ PASETO ซึ่งออกแบบมาเพื่อความปลอดภัยเป็นตัวเลือกที่ดีกว่า แต่ก็ไม่ควรใช้สำหรับเซสชัน
สรุปประเด็นสำคัญ
- ไม่ควรใช้ JWT เพื่อคงสถานะการล็อกอินของผู้ใช้ และสำหรับจุดประสงค์นี้คุกกี้เซสชันแบบปกติเป็นเครื่องมือที่ดีกว่า
- JWT ไม่ได้ถูกออกแบบมาสำหรับจุดประสงค์นี้ อีกทั้งไม่ปลอดภัย และสำหรับการคงเซสชันล็อกอิน คุกกี้เซสชัน แบบมาตรฐานเหมาะสมกว่า
- ในประเด็นที่เกี่ยวข้อง ไม่ควรเก็บข้อมูลรับรองการยืนยันตัวตน รวมถึงโทเค็น JWT ไว้ใน localStorage หรือ sessionStorage
- มีวิดีโอประกอบเกี่ยวกับ JWT ให้ดูได้ แต่หัวข้ออื่นอย่างการป้องกัน CSRF มักถูกพูดถึงแบบสั้น ๆ จึงควรศึกษาเพิ่มเติมจากแหล่งอื่นแยกต่างหาก
- แม้แต่กรณีใช้งาน JWT ที่ “ใช้ได้จริง” ในช่วงท้ายวิดีโอก็ยังจัดการได้ง่ายกว่าด้วยเครื่องมือที่ดีกว่าและปลอดภัยกว่า โดยเฉพาะ PASETO
เหตุผลที่ควรหลีกเลี่ยง JWT
- สเปกของ JWT ถูกออกแบบมาสำหรับ โทเค็นอายุสั้นมาก ประมาณไม่เกิน 5 นาทีเท่านั้น แต่เซสชันจำเป็นต้องมีอายุยาวกว่านั้น
- การยืนยันตัวตนแบบไร้สถานะ ที่ปลอดภัยนั้นเป็นไปไม่ได้ และหากต้องการจัดการโทเค็นอย่างปลอดภัย จำเป็นต้องมีสถานะบางอย่างเสมอ
- หากต้องมีที่เก็บข้อมูลอยู่แล้ว การเก็บข้อมูลทั้งหมดก็ยังดีกว่าการเก็บเฉพาะสถานะของโทเค็นบางส่วน
- ปัญหาที่เกี่ยวข้องอธิบายไว้อย่างละเอียดกว่าที่ http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
- ในโลกจริงมีแอปพลิเคชันที่ใช้ JWT ในลักษณะนี้อยู่ แต่แอปเหล่านั้นมีข้อบกพร่อง จึงไม่ควรทำผิดแบบเดียวกันซ้ำ
- JWT ที่เก็บแค่ โทเค็นเซสชัน แบบเรียบง่ายนั้นไม่มีประสิทธิภาพและยืดหยุ่นน้อยกว่าคุกกี้เซสชันทั่วไป โดยไม่ได้ให้ข้อดีเพิ่มเติมใด ๆ
- ตัวสเปก JWT เองไม่ได้รับความไว้วางใจจากผู้เชี่ยวชาญด้านความปลอดภัย จึงควรถูกตัดออกจากการใช้งานด้านความปลอดภัยและการยืนยันตัวตนทั้งหมด
- สเปกดั้งเดิมเคยเปิดช่องให้สร้างโทเค็นปลอมได้ และอาจยังมีข้อผิดพลาดอื่นซ่อนอยู่
- ปัญหาของสเปกตระกูล JWT อธิบายเชิงลึกกว่าใน JWT: The JSON Web Token standard is bad and everyone should avoid it
ข้อโต้แย้ง
- ข้อโต้แย้งว่า “Google ก็ใช้ JWT” ไม่ได้หมายถึงเซสชันผู้ใช้บนเบราว์เซอร์
- Google ไม่ได้ใช้ JWT กับเซสชันผู้ใช้บนเบราว์เซอร์ แต่ใช้คุกกี้เซสชันแบบปกติ
- JWT ถูกใช้เพียงเป็นวิธีส่งผ่าน Single Sign On เพื่อถ่ายทอดเซสชันล็อกอินของเซิร์ฟเวอร์หรือโฮสต์หนึ่งไปยังอีกเซิร์ฟเวอร์หรือโฮสต์หนึ่ง
- วิธีใช้งานแบบนี้ถือเป็นกรณีใช้งานที่สมเหตุสมผลของ JWT
- Google มีทรัพยากรผู้เชี่ยวชาญด้านความปลอดภัยมากพอที่จะสร้างและดูแลการใช้งาน JWT ที่ปลอดภัยกว่า
- JWT ของ Google ในทางปฏิบัติแทบไม่เหมือน JWT ที่ใช้กันที่อื่น
- ข้อโต้แย้งว่า “ไร้สถานะดีกว่า” ไม่สอดคล้องกับข้อกำหนดของการยืนยันตัวตนที่ปลอดภัย
- หากไม่มีทรัพยากรมหาศาล ก็แทบเป็นไปไม่ได้ที่จะทำการยืนยันตัวตนแบบไร้สถานะอย่างแท้จริงให้ปลอดภัย
- ดูการอภิปรายที่เกี่ยวข้องได้ที่ Stateless is a lie
- ปัญหาว่า “ไม่รู้จะตั้งค่าเซสชันอย่างไร” มักแก้ได้ด้วยเอกสารและตัวอย่างการใช้งานของเว็บเซิร์ฟเวอร์เฟรมเวิร์กส่วนใหญ่
- เทคโนโลยีเซสชันไม่ใช่เรื่องใหม่เป็นพิเศษ จึงไม่ค่อยเห็นบทความที่อธิบายเซสชันบ่อยนัก
- เพียงแค่อ่านเอกสารของตัวติดตั้งใช้งานเซสชันก็ควรทำตามขั้นตอนการตั้งค่าได้
- เว็บเซิร์ฟเวอร์เฟรมเวิร์กแทบทั้งหมดมีระบบเซสชันมาให้ และถึงจะไม่ได้เปิดใช้โดยค่าเริ่มต้นก็มักเปิดใช้งานได้ง่าย
- Express และเฟรมเวิร์ก Node.js อื่น ๆ ค่อนข้างเป็นข้อยกเว้นอยู่บ้าง เพราะมีความเป็นโมดูลาร์สูงและมุ่งทำหน้าที่เฉพาะอย่าง
- ใน Express ก็เพียงใช้มิดเดิลแวร์
express-sessionและ store connector ที่ตรงกับที่เก็บข้อมูล - แนะนำให้ใช้
connect-session-knexร่วมกับ Postgres, MySQL หรือถ้าเป็นไปได้ก็คือ SQLite
โทเค็นระยะสั้น
- หากมีกรณีที่ต้องใช้โทเค็นแบบเซ็นลายเซ็นอายุสั้นจริง ๆ ก็มีสเปกที่ดีกว่าและออกแบบมาเพื่อความปลอดภัยชื่อ PASETO
- ถึงจะใช้ PASETO ก็ไม่ควรนำไปใช้สำหรับ เซสชัน
วิธีการทำงานของเซสชัน
- หากต้องการเรียนรู้เพิ่มเติมว่าเซสชันทำงานอย่างไร ควรอ่าน gist ของ joepie91
2 ความคิดเห็น
JWT เป็นวิธีลดการเข้ารหัสโทเค็นและการ query DB ไม่ใช่แนวคิดที่อยู่คนละขั้วกับการยืนยันตัวตนด้วยคุกกี้ หากเก็บ JWT ไว้ใน secure cookie ความเสี่ยงจากการถูกขโมยก็จะเท่ากับวิธียืนยันตัวตนด้วยคุกกี้แบบ legacy
การจัดการรายการหมดอายุเพื่อทำให้ JWT expired มีข้อได้เปรียบด้านประสิทธิภาพ เพราะมีความต่างของต้นทุนระหว่างการ query เฉพาะข้อมูลการหมดอายุจาก redis กับการ query สมาชิกทั้งหมดจาก DB
การ query แบบอิง index บน 100,000 member rows (วิธีคุกกี้แบบ legacy) หลายหมื่นครั้ง
vs
การ query รายการหมดอายุ 50 รายการจาก redis (วิธีทำให้ JWT expire ได้ทันที) หลายหมื่นครั้ง
JWT มีข้อดีอยู่ เพียงแต่ในสภาพแวดล้อมขนาดเล็กความแตกต่างอาจเห็นได้ไม่ชัด
ความเห็นจาก Hacker News
ขาดบริบทสำคัญไป: นี่กำลังพูดถึง เซสชันผู้ใช้บนเบราว์เซอร์
สำหรับการสื่อสารระหว่างบริการ หลายกรณีก็ใช้ JWT ได้ดี
เพิ่มเติมคือผมลองอ่านบางส่วนของบทความที่ลิงก์ไว้ แล้วเจอบทความอย่าง https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba... ด้วย ถ้า JWT เป็นมาตรฐานที่แย่และไม่ปลอดภัยขนาดนั้น ก็ลองเปิดเผยวิธีแฮ็ก AssumeRoleWithWebIdentity ของ AWS STS ดูสิ หรือไม่ก็ไม่ต้องเปิดเผย แล้วแอบรันตัวขุดคริปโตบนบัญชี AWS โปรดักชันของบริษัท Fortune 500 ทีละเจ้าไปเลย ถ้า JWT ไม่ปลอดภัยขนาดนั้น ก็รอฟังตอนคุณทำสำเร็จแล้วกัน /ประชด
ส่วนของลายเซ็น/การเข้ารหัสใน JWT นั้นซับซ้อน และถึงตอนนี้ไลบรารี JWT ที่ใช้กันทั่วไปส่วนใหญ่เพิ่งจะมีสติดีขึ้น แต่เมื่อก่อนไม่ได้เป็นแบบนั้น มีไลบรารีจำนวนมากที่ยอมรับอัลกอริทึม
"none"[1] และบางกรณีก็ใช้ public key ราวกับเป็น shared secret จนผู้โจมตีปลอมโทเค็นได้ [2] ซึ่งทั้งหมดนี้เป็นผลโดยตรงจากความซับซ้อนที่บทความต้นทางกำลังวิจารณ์JWT ยังอาจทำสิ่งที่ต้องการในเซสชันผู้ใช้ไม่ได้ด้วย ถ้าไม่มี revocation list เก็บไว้ที่ไหนสักแห่ง ก็ทำให้เป็นโมฆะไม่ได้ แต่ถ้าทุกคำขอจำเป็นต้องเอาตัวระบุไปเทียบกับ revocation list อยู่แล้ว ก็ใช้ session ID แบบทึบแสงแล้วค่อย lookup ทุกคำขอก็ได้ แน่นอนว่าคุณอาจใช้โทเค็นอายุสั้นแล้วรีเฟรชต่อเนื่องก็ได้ แต่สำหรับแอปพลิเคชันทั่วไปที่ยังไงก็ต้องมี state อยู่แล้ว เหตุผลที่จะทำแบบนั้นก็มีไม่มาก
อย่างไรก็ตาม ผมเห็นด้วยเต็มที่ว่าในระบบกระจายหรือการสื่อสารระหว่างเครื่อง โทเค็นที่มีลายเซ็นมีประโยชน์ในหลายกรณี อย่าเอาสองกรณีนี้มาปนกัน
[1] https://nvd.nist.gov/vuln/detail/cve-2022-23540
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-54150
ตอนนี้ไลบรารีหลักในหลายภาษามีค่าเริ่มต้นที่สมเหตุสมผลขึ้นมาก ดังนั้นทุกวันนี้ผมคิดว่ามันค่อนข้างปลอดภัยจริง ๆ แล้ว
ถ้าคำว่า “ปลอดภัยถ้าคุณจับมันถูกและใช้มันถูก” แปลว่าเป็นการออกแบบที่ดี งั้นตรรกะเดียวกันก็น่าจะใช้กับ X.509 ได้เหมือนกัน
ในหลายกรณีมีทางเลือกที่ดีกว่า session token มาตรฐานหรือ API key ถูกใช้กันแพร่หลายในเว็บไซต์ขนาดใหญ่ส่วนมาก และแทบจะเหมาะสมสมบูรณ์แบบกับการใช้งานส่วนใหญ่
ไม่ได้จะบอกว่ามาตรฐานพวกนี้ไม่มีคุณค่าเลย จุดเด่นที่สุดคือมันเป็นมาตรฐานพื้นฐานสำหรับแลกเปลี่ยนบางอย่างโดยไม่ต้องพึ่งการเข้ารหัส ASN.1 และเครื่องมือฝั่ง ASN.1 ก็ดูเปราะบางและมีบั๊กเยอะมาก
ตัวอย่างเช่น ผมไม่รู้ว่าจะโจมตี SAML อย่างไร แต่ผมรู้ว่ามันเป็นมาตรฐานที่แย่มาก เพราะมันทำให้ XML parser ทั้งก้อนกลายเป็นพื้นผิวการโจมตี ผมไม่ใช่นักวิจัยด้านความปลอดภัย จึงไม่รู้ว่าจะหาช่องโหว่ใน XML parser ได้อย่างไร แต่ก็ยังพอรู้ได้ว่าพื้นผิวการโจมตีที่ใหญ่เป็นเรื่องไม่ดี
บอกว่า JWT ไม่ปลอดภัยนี่หมายถึงอะไร แม้จะใช้การเซ็นแบบ RSA/public key จากแหล่งที่เชื่อถือได้ก็ยังไม่พอหรือ? ไม่ได้ใช้ shared secret ด้วยซ้ำ?
ข้ออ้างว่า JWT มีอายุยาวเกินไปก็ดูแปลก คุณกำหนดอายุ JWT ให้สั้นและมีโมเดลการรีเฟรชกับหน่วยงานยืนยันตัวตนได้อยู่แล้ว ต่อให้ใช้เซสชันแบบคุกกี้ คุณก็ยังต้องเก็บอะไรบางอย่างไว้ที่ไหนสักแห่งอยู่ดี คุณทำให้ JWT ใช้ได้แค่ 5–15 นาทีได้ และ 15 นาทีก็ใกล้เคียงกับเวลาแคชของระบบมอบสิทธิ์หลายตัวรวมถึง Entra ด้วย แม้แต่โทเค็นอายุ 5 นาที ถ้ามีระบบรีเฟรชก็ใช้งานบนเบราว์เซอร์ได้สบาย
สุดท้ายแล้ว ผมชอบแยกเรื่องตัวตน/การยืนยันตัวตนออกจากบริการแอปพลิเคชัน·API จะได้ดึง context ออกไปภายนอกได้ และวิธีจัดการ JWT ในทุกคำขอก็ดูรับมือได้ง่ายกว่าระบบ shared cache/state ที่อาจล้มเหลวเป็นครั้งคราว โทเค็นที่มีลายเซ็นสามารถตรวจสอบลายเซ็นกับหน่วยงานที่รู้จักได้
นอกเหนือจากนั้น ลายเซ็นก็ถูกต้องในเชิงเข้ารหัสลับ คุณเพียงแค่ต้องตรวจสอบ JWT ทุกครั้งด้วยอายุที่สั้น
อนึ่ง โทเค็น OIDC ทั้งหมดก็คือ JWT
ถ้าเทียบเซสชันกับ revocation list ของ JWT ก็ยังมีตรรกะที่เข้าข้าง revocation list ของ JWT อยู่เหมือนกัน JWT มีเวลาหมดอายุที่จำกัด ดังนั้นคุณต้องเก็บ revocation list เฉพาะโทเค็นที่ยังไม่หมดอายุเท่านั้น
เมื่อเทียบกับ JWT ที่ยังใช้การได้ทั้งหมด โทเค็นที่ถูกเพิกถอนน่าจะเป็นแค่ส่วนน้อย ดังนั้นในทุกคำขอคุณจึงต้อง lookup แค่ชุดข้อมูลขนาดเล็กมาก
ถ้าใช้เซสชัน รายการเซสชันที่ใช้งานได้อาจมีขนาดใหญ่กว่า revocation list หลายหลักของจำนวน และด้วยเหตุนี้ทั้งต้นทุนการเก็บ state และต้นทุน lookup ก็สูงกว่า
อีกอย่าง บทความบอกว่า JWT เป็น stateless แต่โดยปกติไม่ค่อยเป็นแบบนั้น ส่วนมากคุณไม่ได้แค่ตรวจสอบ JWT แต่ยังดึง identity object ที่ตรงกันกลับมาทุกคำขอ เช่นรายละเอียดผู้ใช้ เพื่อตรวจว่าผู้ใช้ยัง active อยู่ไหม และยังมีสิทธิทำงานนั้นอยู่ไหม คุณอาจใช้ revocation list ระดับผู้ใช้หรือค่าอย่าง
minimum_issued_atเพื่อตรวจสอบฟิลด์iatของ JWT ได้ แบบนี้ก็รองรับแพตเทิร์น “ออกจากระบบทุกอุปกรณ์” ได้ด้วย โดยการตั้งminimum_issued_atของผู้ใช้เป็น$NOWก็จะเพิกถอนโทเค็นเก่าทั้งหมดได้ทันที โดยไม่ต้อง lookup revocation list รายตัวselectที่ใช้อินเด็กซ์จากฐานข้อมูลครั้งหนึ่ง และคืนค่า 0~1 แถว ในกรณีส่วนใหญ่มันไม่ใช่เรื่องที่ต้องกังวลบทความนี้ลิงก์ไปยังโพสต์บล็อกอื่นสำหรับประเด็นส่วนใหญ่ในส่วน “ทำไม” และดูเหมือนว่าโพสต์เหล่านั้นจะไม่พอใจเป็นหลักกับเรื่องที่ว่า “ไม่สามารถเพิกถอนโทเค็น JWT รายตัวได้”
ทุกครั้งที่ผมต้องนำไปใช้งาน แนวทางทั่วไปก็คือเช็ก nonce ที่ถูกเพิกถอนไว้จากที่ไหนสักแห่ง ซึ่งถ้าทำแบบนั้นก็จะแก้ข้ออ้างข้อที่สองในโพสต์นั้นด้วย
คำกล่าวที่ว่า “ผู้เชี่ยวชาญด้านความปลอดภัยเองก็ไม่เชื่อถือสเปก JWT” ดูเหมือนต้องมีหลักฐานมากกว่าบล็อกโพสต์แค่ชิ้นเดียว และโพสต์นั้นก็ดูจะโทษการติดตั้งใช้งานที่แย่มากกว่า ซึ่งไม่ว่ามาตรฐานไหนก็หนีปัญหาการนำไปใช้ผิด ๆ ไม่พ้น
โดยรวมแล้ว ผมก็ไม่รู้เหมือนกันว่าตัวเองคาดหวังอะไรจากการกดลิงก์ gist สุ่ม ๆ
นอกจากนี้ยังใช้ JWT อายุสั้นมากพอในเบราว์เซอร์ได้ และทำให้เอเจนต์ต่ออายุเองได้ด้วย ถ้าใช้ Azure Entra หรือผู้ให้บริการอื่น ๆ หลายราย มันก็ทำงานแบบนั้นอยู่แล้ว คุณสามารถตั้ง JWT ให้สั้นพอสมควร เช่น 5–15 นาที และยังตรวจสถานะการเพิกถอน
jtiได้ด้วยJWT มีประโยชน์มากในการแยกและนำหน่วยงานออกสิทธิ์การเข้าถึงกลับมาใช้ซ้ำจากระบบแอปพลิเคชัน/API มันคือการย้ายพื้นผิวการโจมตีไปยังจุดอื่น แต่ย้ายไปในแบบที่ไว้ใจได้ ทั่วโลกเองก็ใช้แนวทางกุญแจสาธารณะกันในหลายที่ รวมถึง SSH ด้วย ผมจะไม่ใช้ shared secret หรือโทเค็นอายุยาว แต่โทเค็นอายุสั้นที่เซ็นด้วยกุญแจสาธารณะจากแหล่งที่ตรวจสอบแล้วและเป็นที่รู้จักนั้น โดยทั่วไปก็โอเค
กลับกัน สิ่งที่มักเป็นปัญหาจริง ๆ คือ API key มากกว่า ผมเพิ่งต้องทำระบบนี้ไป ในกรณีของผมก็ทำให้ API key ดูเหมือน Bearer token โดยมีคำนำหน้า
sak.สั้น ๆ ตามด้วยส่วนระบุตัวตน (ไบต์ UUID แบบ base64url) แล้วต่อด้วยค่าส่วนลับ (ไบต์แบบ base64url) ในฐานข้อมูลจะเก็บ UUID และ salt+hash ระดับ passphrase ที่สร้างจากค่าส่วนลับ ดังนั้น API key ที่สร้างขึ้นจึงต้องถือเป็นความลับ และในฐานข้อมูลก็เก็บได้แบบทางเดียวเท่านั้น ทำให้ฐานข้อมูลรั่วไม่ได้แปลว่าการยืนยันตัวตนจะพังตามไปด้วยถึงอย่างนั้น การรั่วของ API key ก็ยังมีโอกาสเกิดขึ้นได้มากกว่าที่โซลูชัน JWT ที่ทำมาดีจะมีปัญหาเสียอีก
ผมบังเอิญเจอโพสต์นี้ และเพราะเมื่อก่อนเคยทำงานกับหัวข้อนี้เยอะมาก ก็เลยคิดว่าน่าสนใจดีที่มันกลับมาเป็นประเด็นอีกครั้งตอนนี้ แต่พอกดเข้าไปดู ผู้เขียนกลับลิงก์ไปยังงานบางส่วนของผมเอง ความทรงจำเก่า ๆ ผุดขึ้นมาเลย
อย่างไรก็ดี แม้จะมีคนที่ฉลาดกว่าผมมากจัดการประเด็นนี้กันอย่างกว้างขวางมาหลายปีแล้ว แต่แม้ในปี 2026 ผมก็ยังคิดว่า JWT เป็นเครื่องมือที่ไม่เหมาะกับ web authentication มันโอเคสำหรับการใช้งานระหว่างบริการ แต่ถ้ามีสิทธิ์เลือก ก็ใช้ PASETO ไปเลยดีกว่า มันแก้ปัญหาได้หลายอย่าง
https://www.paseto.io/
ตอนนี้กำลังเอา RabbitMQ มาใช้กับระบบ push notification ของเว็บไซต์ และใช้การยืนยันตัวตนด้วย JWT เพื่อควบคุมว่าฝั่งไคลเอนต์จะอ่านอะไรได้จากที่ไหน โดยตั้งอายุสั้นและมีการต่ออายุโทเค็นเป็นระยะ
ผมนึกไม่ค่อยออกว่ามีการตั้งค่าแบบอื่นที่เข้าใกล้ความง่ายของแบบนี้ได้แค่ไหน แค่เพิ่ม endpoint หนึ่งตัวที่ออกโทเค็น JWT ให้กับเซสชันที่ยังใช้ได้ก็จบ และยังทำสิทธิ์แยกรายผู้ใช้ได้ด้วย
หนึ่งในบทความที่ลิงก์มาเพื่ออธิบายว่าทำไมไม่ควรใช้ JWT นั้น อย่างดีที่สุดก็ถือว่าแปลก
https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba...
สรุปสั้น ๆ คือ “ไลบรารีบางตัวมีบั๊ก” แล้วจากนั้นก็ลาก libsodium เข้ามาแล้วแนะนำให้ทำเอง ซึ่งเป็นคำแนะนำที่เหลวไหลจนยากจะมองว่าเอาจริง ซอฟต์แวร์ทุกอย่างมีบั๊กทั้งนั้น ตอน Heartbleed อินเทอร์เน็ตทั้งโลกก็ปั่นป่วน แต่เราก็ยังใช้ TLS และ OpenSSL กันอยู่
ผมเพิ่งเคยได้ยินคำกล่าวที่ว่า “สเปก JWT ถูกออกแบบมาโดยเฉพาะสำหรับโทเค็นอายุสั้นมาก ประมาณไม่เกิน 5 นาที” และก็หาหลักฐานมารองรับไม่ได้เช่นกัน ใน RFC 7519 ไม่มีคำกล่าวแบบนั้น
ปกติแล้ว JWT ถูกใช้เหมือน authentication cache คุณรับโทเค็นยืนยันตัวตนจากบริการยืนยันตัวตน แล้วโทเค็นนั้นก็มอบสิทธิ์ให้กับบริการอื่น ๆ
ข้อดีมีหลายอย่าง แต่แก่นสำคัญคือบริการลูกไม่จำเป็นต้องโต้ตอบกับฐานข้อมูลยืนยันตัวตน และไม่จำเป็นต้องมีสิทธิ์ออกโทเค็นเอง โดยสมมติว่าใช้ RS256 ไม่ใช่ HMAC ดังนั้นถึงบริการลูกจะถูกเจาะ ก็ยังไม่ร้ายแรงเท่ากับบริการที่เข้าถึงฐานข้อมูลยืนยันตัวตนได้ถูกเจาะ
ถ้ามีข้อมูลอ่อนไหวอยู่ในโทเค็นก็ควรใช้ JWE แต่ข้อเสียคือทุกครั้งที่ใช้งาน คุณต้องขอให้บริการภายในที่ถือ private key ถอดรหัสโทเค็นให้ ซึ่งไม่ได้ดีนัก
โครงสร้างที่ผมใช้บ่อยคือ
{"id": (uuid), "scopes": ["scope:read/write"]}มันยังเหมาะกับ SPA พอสมควรด้วย เพราะเซิร์ฟเวอร์ไซต์แบบ static สามารถตรวจสอบ JWE ด้วย public key ได้ก่อนจะส่ง resource แนวทางของผมคือคอมไพล์ไซต์แบบ static ให้อยู่ในรูป
/(scope)/pathและทำให้บริการ static ไม่ส่งหน้าที่ไม่มีสิทธิ์เข้าถึงตั้งแต่แรก มีประโยชน์มากเวลาคุณไม่อยากเปิดเผยฟังก์ชันฝั่งแบ็กเอนด์หรือเส้นทางบริการภายในที่อาจถูกโจมตีได้ให้ผู้ใช้เห็น เช่น แผงผู้ดูแลระบบJWT สำหรับ “เข้าถึงแบ็กเอนด์” มีอายุประมาณ 5 นาที และสิ่งอย่าง
/meจะถูกแคชไว้ใน localStorage เว้นแต่ว่า/refreshจะระบุชัดให้ทิ้งแคชใน localStorage ตัวจัดการคำขอของแอป SPA จะตรวจจับว่า “ต้องรีเฟรช” แล้วต่ออายุโทเค็นผมคิดว่าความรับผิดชอบส่วนใหญ่อยู่ที่ไลบรารี node/next และ Python แบ็กเอนด์ควรเขียนด้วยภาษาที่ strongly typed และฟรอนต์เอนด์ก็ควรเป็นหน้า static ที่ precompile ไว้เสมอ ตอนนี้ชุดฟรอนต์เอนด์ของผมใช้ VITE โดยให้หน้า landing เป็นหน้า prerendered และตัวแอปเป็น SPA ปกติ
ถึงจะพิจารณาทั้งหมดนี้แล้ว ผมก็ยังไม่เห็นด้วยอย่างมากกับ gist ทั้งชิ้นนี้ JWT ทำให้ปลอดภัยได้มากเท่าที่คุณอยากทำให้มันปลอดภัย
JWT ก็โอเค เพียงแต่พาดหัวดู เรียกกระแส ไปหน่อย
แต่ก็มีประเด็นที่น่าคุย เช่น ควรใช้ค่าแบบเข้ารหัส (สมมาตรหรืออสมมาตร), ค่าสุ่มที่เป็นความลับ, หรือค่าที่เซ็นกำกับไว้ (อ่านได้แต่แก้ไขไม่ได้) เมื่อไร ควรเก็บค่าเหล่านี้ไว้ที่ไหน (หน่วยความจำ, localStorage, คุกกี้) และจะทำอย่างไรไม่ให้ค่าพวกนี้อยู่ตลอดไป รวมถึงจำเป็นต้องเพิกถอนก่อนเวลาหมดอายุตามธรรมชาติหรือไม่