JSON Web Token มาตรฐานใหม่ ในการทำ Authentication

เหตุการณ์สมมติ: คุณเป็น Developer มือหนึ่งของบริษัท, หัวหน้าของคุณต้องการสร้าง Web Application ที่ต้องรองรับผู้ใช้งานกว่า 10 ประเทศทั่วโลก โจทย์แรกที่หัวหน้าคุณให้มาคือ ระบบ Authentication…

ถ้าถูกใจบทความ ฝากกดไลค์ Facebook Page เตาะแตะต๊อกแต๊ก ทีนะคร๊าบบ https://www.facebook.com/rootusercc/ เป็นกำลังใจให้ผู้เขียนสุดพลัง :-)

Authentication คืออะไร?

Application เกือบทุกตัว จำเป็นต้องรู้จักผู้ใช้ที่กำลังใช้งานอยู่ว่า ใครคือผู้ใช้งาน ซึ่งระบบที่ทำหน้าที่ตรงนี้นี่แหละ เราเรียกว่า Authentication

ขั้นตอนพื้นฐานเลยก็คือ ผู้ใช้กรอก Username & Password แล้วนำไปเปรียบเทียบกับ Database ว่าตรงกับใคร, Password ที่กรอกถูกต้องหรือไม่? หากข้อมูลตรงกัน ก็สามารถยืนยันได้ว่า ผู้ใช้คนนั้นเป็นใคร

บางทีอารมณ์นี้เลย

สิ่งที่ตามมาหลังจากทำ Authentication ก็คือ การทำ Authorization หรือการตรวจสอบสิทธิต่างๆ เช่น ผู้ใช้คนนี้ มีอำนาจอะไรบ้าง อาจจะดูข้อมูลได้อย่างเดียว แต่หากเป็นระดับ Manager, สามารถแก้ไขข้อมูลได้ เป็นต้น

ความท้าทายของการทำ Authentication ใน Web Application

ก็คือ การที่ HTTP Protocol มันขี้ลืมมากกก…. เราเรียกอาการของมันว่า Stateless ครับ…

Stateless หมายถึงรูปแบบการสื่อสารที่ไม่จดจำครั้งก่อนหน้าเลย… พูดง่ายๆ คือ ทุก Request ใน HTTP ที่ User ยิงเข้า Server เป็นเรื่องใหม่ตลอด ไม่มีข้อมูลใดๆ จาก Request เก่าทั้งสิ้น

ซึ่งหมายความว่า… หลังจาก User Login สำเร็จแล้ว…. พอ User จะเข้าไปดูข้อมูลส่วนตัวใน Request ถัดมา… Server ก็จำไม่ได้แล้วว่า User คนนี้เพิ่ง Login ไป :-(

ขี้ลืมแบบนี้ ตังค์หมดก่อนแน่นอน

สิ่งที่ Developer ต้องทำก็คือ… ส่งข้อมูลบางอย่าง บอก Server ใน ทุกๆ Request หลังจากที่ Login สำเร็จ เพื่อให้ Server “ไม่ลืม” ว่า… User คนนี้เพิ่ง Login สำเร็จนะ :-)

วิธีแบบเดิมๆ ที่เราทำๆ กันอยู่ ก็คือ การหนีบ Session ID ของ User แต่ละคน ส่งไปให้ Sever ผ่าน Cookie นั่นเอง ซึ่งเราเรียกวิธีการแบบนี้ว่า “Server Based Authentication”

การ Authentication ของ Web Application แบบ Server Based Authentication (ภาพจาก https://www.toptal.com/web/cookie-free-authentication-with-json-web-tokens-an-example-in-laravel-and-angularjs)

Server Based Authentication

จากรูปภาพประกอบข้างบน เราจะเห็นว่า มีการ Request มาที่ Server 2 Requests

  1. Authentication Request: User ส่ง​ Credential (Username / Password) ของตัวเองไปให้ Server, เมื่อ Server เช็คว่าข้อมูลถูกต้อง ก็สร้าง Session ID เก็บไว้ใน Server (อาจจะใน Memory หรือ Database) พร้อมกับส่ง Session ID อันเดียวกันนี้ กลับไปให้ผู้ใช้ จากนั้น Browser ก็บันทึก Session ID ลงไปใน Cookie
  2. Subsequent Request: อย่างที่บอกว่า HTTP เป็น Stateless, Request ถัดๆ มาของผู้ใช้คนนี้ เลยต้องหนีบ Session ID ที่เพิ่งบันทึกไว้ใน Cookie, ส่งให้ Server ทุกครั้งไป Server ก็จัดการ หยิบ Session ID นั้น มาเช็คกับรายการ Session ID ว่าเป็นของจริงหรือเปล่า? แล้วเป็นของผู้ใช้คนไหน? ถ้าเจอก็จัดการส่งข้อมูลที่ผู้ใช้ร้องขอ แต่ถ้าไม่เจอก็แปลว่า เป็นผู้ใช้ปลอม ส่ง Error 401 Unauthorized กลับไปหา User ได้เล้ย

แล้วปัญหาคืออะไรนะ ?

เป็นธรรมดาของโลกโปรแกรมมิ่ง… เทคโนโลยีเก่าๆ ค่อยๆ ถูกทดแทนด้วยเทคโนโลยีใหม่ๆ เรามาดูข้อเสียของระบบเดิมกันก่อนดีกว่า ว่ามันมีข้อจำกัดอะไรบ้าง

ประสิทธิภาพ (Performance)

อย่างแรกเลยคือ มัน “เปลือง” ครับ… ย้อนกลับไปตรง Subsequent Request, คุณจะเห็นว่า เมื่อ Server ได้ Session ID มา… สิ่งที่มันต้องทำต่อก็คือ การ Request ไปที่ส่วนเก็บข้อมูล (อาจจะอยู่ใน Database หรือใน Memory ก็ได้) นับเป็น 1 การทำงาน แต่อาจจะมีแถมอีก 1 Query ก็คือ เรื่องของการดูสิทธิ (Roles & Permission) เพื่อใช้ในเรื่องการทำ Authorization ด้วยครับ

การขยายตัว (Scalability)

จากเรื่องประสิทธิภาพ, มันก็เลยทำให้ Scale ลำบาก… เพราะถ้าคุณเก็บ Session List ทั้งหมดไว้ใน Memory ที่ Server ตัวใดตัวหนึ่ง, พอถึงคราวที่คุณต้องติดตั้ง Server เพิ่มขึ้นอีกตัว… แน่นอนว่า Memory สองตัวมันก็ไม่ Sync กัน… การ Scale App ก็ยากลำบากขึ้นไปอีก

แถมอีกนิดคือ ส่วนใหญ่แล้ว เวลาเราใช้ Framework มันก็จะแถมการ Authentication แบบ Server-based มาให้ด้วย ซึ่งเป็นการผูกมัดเทคโนโลยีไปในตัว… ขัดกับหลัก Decoupled ซึ่งจะมีผลต่อการ Scale ในอนาคตเช่นกัน

CORS (Cross-origin Resource Sharing)

บางครั้ง คุณอาจจะต้องขอข้อมูลจาก Service อื่นๆ ของบริษัทคุณเอง แต่อยู่คนละ Domain การ Authentication แบบเดิม ที่เก็บ Identity ไว้ใน Cookie จะมีปัญหา เพราะการ Request ข้าม Domain แบบนี้ จะไม่ได้เอาค่าต่างๆใน Cookie ส่งพ่วงไปให้ด้วย ทำให้การระบุตัวตน กับ Service ที่คุณกำลังขอข้อมูลไม่สำเร็จครับ

Cross-Site Request Forgery (CSRF, XSRF)

อันนี้เป็นเรื่องของความปลอดภัยครับ… การทำ Server Based Authentication จะทำให้ทุกๆ Request ที่ตามหลังมา, ถูกหนีบ Session ID ไปด้วยเสมอ ซึ่งหมายความว่า หากเว็บไซต์ใดๆ เปิดช่องโหว่ในการโจมตีแบบ CSRF นี้ จะทำให้ Hacker สามารถหลอกล่อให้เหยื่อ Click Link ที่เป็นกับดัก เพื่อให้เกิดผลลัพธ์ตามที่ Hacker ต้องการได้ (เช่น… หลอกให้ผู้ใช้ Click Link http://www.stupidbank.com?transferto=TheifAccount&amount=9999 เพื่อทำการโอนเงิน เป็นต้น)

อันนี้เป็นช่องโหว่ที่เล่นงานเว็บดังๆ ไปเยอะเหมือนกันนะครับ อย่าง Youtube นี่ทำให้ User Subscribe หรือกดไลค์ Video โดยที่เจ้าตัวไม่รู้ตัว…. หรือ uTorrent ที่ทำให้ User โหลดไฟล์ Torrent โดยอัตโนมติ (สนใจอ่านเพิ่มเติมที่นี่เลยครับ http://broom02.revolvy.com/main/index.php?s=Cross-site%20request%20forgery)

ภาพตัวอย่าง Flow การโจมตี แบบ CSRF

โอเค ซื้อละ, แล้ว JWT มันจะมาช่วยยังไง ?

อ่านมาถึงตอนนี้ บางคนอาจจะเริ่มเห็นข้อจำกัดของการทำ Authentication แบบเก่า งั้นมาเริ่มดูข้อดีของ JWT กันเลยดีกว่า

JWT คืออะไร ?

JSON Web Token (JWT) เป็นมาตรฐานเปิด (RFC 7519) ที่เข้ามาแก้ปัญหาการส่งข้อมูลอย่างปลอดภัยระหว่างกัน โดยที่ถูกออกแบบไว้ว่า จะต้องมีขนาดที่กระทัดรัด (Compact) และเก็บข้อมูลภายในตัว (Self-contained)

หน้าตาของ JWT

โครงสร้างหน้าตาของเจ้า JWT

JWT ก็เป็น Token หรือ ชุดตัวอักษรชุดหนึ่ง โดยมีโครงสร้างแบ่งออกเป็น 3 ท่อน ได้แก่

  1. Header: ไว้เก็บว่า ข้อความชุดนี้ เข้ารหัสแบบไหนอยู่ (เช่น SHA256, RSA)
  2. Payload: เก็บข้อมูลจริงๆ เช่น User ID, Roles ของผู้ใช้, E-mail ผู้ใช้ เป็นต้น
  3. Signature: ส่วน Digital Signed ซึ่งเหมือนลายเซ็นทิ้งท้าย ไว้เช็คว่า เป็น Token ที่ถูกสร้างอย่างถูกต้องหรือไม่ เพราะหากมี Hacker จงใจเปลี่ยนข้อมูลใน Payload ทำให้ Signature ไม่ตรง… Token นั่นก็จะไม่ถูกนำมาใช้ เพราะเชื่อถือไม่ได้

มาลองของจริงกันเลยดีกว่า!

ข้างล่างนี้เป็น Token ตัวจริงครับ ให้คุณลอง Copy แล้วนำไปวางไว้ในเว็บไซต์ https://jwt.io/ แล้วมาดูผลลัพธ์กันครับ

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcnRhZXRva3Rhay5jb20iLCJpYXQiOm51bGwsImV4cCI6bnVsbCwiYXVkIjoiIiwic3ViIjoi4LmA4LiV4Liy4Liw4LmB4LiV4Liw4LiV4LmK4Lit4LiB4LmB4LiV4LmK4LiBIn0.WM9PJJGmHxjivWOEy2ZfFnC0DR248HBPpv1tjvIUHjg

เว็บดังกล่าว เป็นเครื่องมือสำหรับการ Debug Token, คุณจะเห็นว่า เอกลักษณ์หนึ่งของ JWT ก็คือ Token จะถูกเข้ารหัสไว้ และสามารถถอดรหัสกลับมา เพื่ออ่านข้อมูลที่ถูกเก็บไว้ได้

แต่…! จากรูปคุณจะเห็นตัวเว็บฟ้องกลับมาว่า “Invalid Signature” นั่นก็เพราะ Signature ไม่ตรงกับที่ผมสร้าง Token นี้ขึ้นมาครับ ซึ่งส่วน Signature นี่เอง ที่ทำให้ Server มั่นใจได้ว่า Token ไหน ควรไว้ใจได้

ให้คุณลองเปลี่ยนข้อความในกล่อง Verify Signature จาก Secret เป็น “tortaetoktak”… ตัวเว็บจะบอกผลว่า “Signature Verified” ซึ่งหมายถึง Token ถูกต้อง เชื่อถือได้เลย

เดี๋ยวนะ… ถอดรหัสกันง่ายงี้ ? แล้วมันปลอดภัยตรงไหน

ตอบสั้นๆ ก็คือ “ไม่ปลอดภัย” ครับ #ฮาครืน….

JWT ไม่ได้ออกแบบให้คุณเก็บข้อมูลที่เป็นความลับอยู่แล้วครับ อย่าง Password, เลขบัตรเครดิต อะไรพวกนี้ ห้าม! เก็บไว้ใน JWT เด็ดขาด

สิ่งที่คุณเก็บใน JWT ก็คือ ข้อมูลที่น้อยที่สุด ที่ใช้ระบุตัวตนในการทำ Authentication / Authorization หรือ ก็พวก User ID กับ Roles นั่นแหละครับ

อ่าโอเค, งั้นต่อเลย… ใช้งานยังไงนะ ?

เนื่องจาก JWT ถูกออกแบบมาให้มีขนาดกะทัดรัด… เวลาใช้งาน เราสามารถพ่วง Token นี้ ไปกับ HTTP Request ได้เลย ผ่าน Header หรือแม้กระทั่ง GET Parameter

เมื่อ Server ได้รับ Token นี้… ก็จัดการเช็ค Signature ว่าถูกต้องไหม และเริ่มการถอดรหัส (Decrypt) เพื่อเอาข้อมูลออกมา… เช่น User ID และ Roles ของ User….

ความเจ๋งของการที่มันเก็บข้อมูลได้นี่เอง ทำให้ Server เป็น Stateless อย่างแท้จริง (ไม่ต้องเก็บรายการ Session ID เหมือนวิธีแรก) ทำให้ลดการทำงานลง (ไม่ต้อง Query หา User และ Permission) เพิ่มประสิทธิภาพให้กับตัวแอพพลิเคชั่นของเราได้ง่ายๆ

การ Authentication ของ Web Application แบบ JWT (ภาพจาก https://www.toptal.com/web/cookie-free-authentication-with-json-web-tokens-an-example-in-laravel-and-angularjs)

จากภาพด้านบน Request แรกที่ทำการ Authentication, Server จะทำการสร้าง JWT แล้วส่งกลับไปให้ Browser ทำการเซฟลง Localstorage

ความแตกต่างตรงนี้ กับวิธีแรก คือ Server จะไม่เซฟ JWT ไว้ที่ Server เลย

เมื่อ User ทำการเรียก Request ถัดไป เพื่อดึงข้อมูลส่วนตัว Browser ก็จะส่ง JWT กลับมาให้ Server ทำการ Decrypt ข้อมูล จัดการเช็คสิทธิ แล้วส่งข้อมูลกลับไปให้ User ในกรณีที่มีสิทธิการใช้งาน

ความแตกต่างตรงนี้ กับวิธีแรก คือ Server ไม่ต้อง Query หาข้อมูลเพิ่มเติม หยิบเอา Payload มาใช้งานได้ทันที

หรือหาก User ต้องการติดต่อ Server ตัวอื่น ก็ใช้ JWT ชุดเดิมเนี่ยแหละ ส่งไปให้, Server ตัวนั้น ก็เช็ค Signature และ Decrypt ข้อมูล เมื่อเช็คแล้ว พบว่าทุกอย่างถูกต้อง…. ก็เปิดข้อมูลส่วนตัวนั้นให้ User ดู #easyมากๆ

ไหนลองเทียบกับการ Authentication แบบเดิมซิ ?

มาดูข้อเสียของวิธีแรก ที่เราเพิ่ง List ไปกันว่า, JWT แก้ไขปัญหามันยังไง

ประสิทธิภาพ (Performance)

เนื่องจากความพิเศษของมัน ที่มีการเก็บข้อมูลในตัว (Self-contained) เลยทำให้มันลดการทำงานซ้ำๆ ไป เช่น การ Query หาข้อมูลผู้ใช้ หรือ Permission ของผู้ใช้

ข้อมูลเหล่านี้ ไม่ได้อันตรายและเสี่ยงต่อความปลอดภัย เราเลยสามารถเก็บลง Payload ได้ ลดการทำงาน เพิ่มประสิทธิภาพ ไม่ต้อง Query หา Database ซ้ำๆ

การขยายตัว (Scalability)

จะขยายไปกี่ Server ก็ไม่มีปัญหา เพราะว่า ไม่ได้เก็บ Token ไว้บน Server เลยแม้แต่ตัวเดียว… แถมแยกส่วน Authentication ออกมาเป็น Service ต่างหาก ไม่ต้องขึ้นอยู่กับโปรเจคใดๆ สนับสนุนเรื่อง Modularity ทำให้ Scale App ในอนาคตได้ง่ายมากๆ

CORS (Cross-origin Resource Sharing)

จะทำเว็บไซต์หลายเว็บ ก็ไม่ต้องกลัวเรื่อง CORS เพราะไม่ต้อง Request ข้ามโปรเจค เพื่อทำการ Authentication… ส่ง Token นี้ไป… Verify Signature หากทุกอย่างถูกต้อง ก็นำไปใช้ต่อได้เลย

Cross-Site Request Forgery (CSRF, XSRF)

Token ไม่ได้เก็บไว้ใน Cookies เหมือนแบบแรก… แต่แนบไปกับ Header หรือ GET / POST Parameters ทำให้ไม่มีโอกาสเกิดการโจมตีแบบ CSRF แน่ๆ สบายใจได้

ฟังดูดีนะ, แล้วข้อเสียมีอะไรบ้างหละ

แน่นอน, ทุกเทคโนโลยี มาพร้อมข้อดีข้อเสีย เรามาดูข้อเสียของการใช้ JWT กันบ้าง

Invalidate Token ไม่ได้

สมมติ… มีโจรขโมยมือถือผู้ใช้คนหนึ่งไป… นั่นเท่ากับว่า เป็นการขโมย Token, หากโจรคนนั้นรู้จักเว็บเรา ก็สามารถหยิบ Token ออกมาจาก Localstorage แล้วนำไปใช้ทำโน้นทำนี้ต่อได้สบายๆ… โชคดีที่ User คนนี้ไหวตัวทัน รีบแจ้งเราซึ่งเป็น Webmaster ให้ช่วย Invalidate Token นั้นทิ้งให้หน่อย… แต่… ทำยังไงนะ…

ถ้าเป็นวิธีแรก Server Based Authentication, เราจะมี List ของ Session ID เก็บไว้… การ Invalidate ก็แค่ ลบ Session ID นั้นทิ้งไป… แค่นี้ ก็ทำให้ Session ID นั้นใช้งานไม่ได้ละ

แต่ของ JWT ยากขึ้นมาหน่อย เพราะเราไม่ได้เก็บ List ของ Tokens ไว้…

เราสามารถแก้ปัญหานี้โดยการทำ List ของ Invalidate Tokens ไว้… เวลาต้องการ Invalidate Token ใด ก็เข้ามาเพิ่มใน List นี้ แต่วิธีการนี้ ก็ต้องเพิ่มขั้นตอนก่อนใช้ Token แต่ละครั้ง เพื่อทำการเช็คกับ List นี้ก่อน… ว่ายัง Valid อยู่หรือไม่

ซึ่งวิธีการนี้ มันก็กลับทำให้ Server กลายเป็น Stateful เหมือนกับวิธีแรก ผิดกับความตั้งใจของ JWT :-(

ข้อมูลเก่ากึ๊ก…

พอมันเป็น Self-contained มันก็แลกมากับข้อมูลไม่ถูกอัพเดท… ให้นึกสถานการณ์ที่คุณปรับ Role ให้ User คนนั้นเป็น Admin… วันต่อมา คุณอยาก Revoke ยึด Role นั้นคืน… สำหรับ JWT แล้ว เป็นเรื่องลำบากยิ่ง…

บทสรุป

โดยส่วนตัวแล้ว ผมมองว่า JWT เป็นมาตรฐานใหม่ ที่จะเข้ามาเปลี่ยนการทำ Authentication อย่าหลีกเลี่ยงไม่ได้แน่ๆ… แต่ก็ยังมีปัญหาบางอย่าง ที่ต้องหาทางแก้ไขกันต่อไป

อย่างไรก็ตาม ใครที่ทำงาน Scale ใหญ่ ๆ ควรที่จะต้องศึกษาไว้ และชั่งน้ำหนัก ดูข้อเสีย ดูความเหมาะสม และเลือกเทคโนโลยีที่ตรงกับความต้องการของแต่ละงานๆ ไปครับ

ถ้าถูกใจบทความ ขอฝากกดไลค์ Facebook Page เตาะแตะต๊อกแต๊ก ทีนะคร๊าบบ https://www.facebook.com/rootusercc/ เป็นกำลังใจให้ผู้เขียนสุดพลัง :-)

ข้อมูลอ้างอิง

แก้ไข 1: แก้ไขปัญหาเรื่อง CORS ในการ Authentication แบบเดิม ที่ถูกต้องคือ ปัญหาของเดิมจะเป็นเรื่องของการดึงข้อมูลข้าม Domain ไม่ใช่การทำ SSO ครับ

แก้ไข 19 สิงหาคม 2559: บทความนี้ ส่วนใหญ่ จะ Base จาก เว็บไซต์นี้ครับ
https://jwt.io/introduction/

ซึ่ง ณ เวลานี้ เหมือนกับว่า เรื่องการเก็บข้อมูล ระหว่างใน localstorage กับใน cookies ยังเป็นที่ถกเถียงกันอยู่ เพราะมันเป็นการแลกช่องโหว่กันระหว่าง XSS กับ XSRF

ฝั่งเก็บลง Localstorage

ฝั่งเก็บลง Cookies