ใช้งาน JWT(JSON Web Tokens) ยังไงให้ปลอดภัย
JWT(Json Web Tokens) คือ มาตรฐานที่บอกวิธีการใช้ Token สำหรับการ Authentication ให้ปลอดภัย โดยจะใช้ JSON ในการรับส่งข้อมูล ซึ่งเราสามารถตรวจสอบว่าข้อมุลที่ฝังอยู่ใน token นั้นถูกแก้ไขหรือไม่(Integrity) กระบวนการทำงานของ JWT จะมีขั้นตอนต่างๆ ตามรูปนี้
ขั้นตอนการทำงานของ JWT
- ถ้า User จะต้องส่ง Username และ Password เข้าไป Authentication ก่อน
- ถ้า Username กับ Password ที่ส่งมาถูกต้อง Server จะทำการ Sign Token(สร้าง Token) ด้วย Secret ดูวิธีการ Sign Token ด้านล่าง
- ส่ง Token กลับไปยัง browser
- หลังจากนี้ถ้าเราต้องการใช้งาน service อะไรเราก็จะส่ง JWT ไปทาง Header
Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd...
- เมื่อ Server ได้รับ JWT Server ก็จะทำการตรวจสอบความถูกต้องของ JWT(Validate Token)
- ส่งผลลัพธ์กลับไปยัง client
Server ที่ทำหน้าที่ Sign และ Validate Token ไม่จำเป็นต้องเป็นตัวเดียวกัน
JWT vs API Key
เราจะใช้ JWT และ API Key ในการทำ Authentication เหมือนกันแต่ความแตกต่างของ JWT และ API Key คือ JWT จะมี expire(หลังจาก expire แล้ว token จะไม่ valid แล้ว) ส่วน API Key คุณต้องยกเลิกเองเมื่อเวลาผ่านไป
JWT หรือ API Key ถ้ายิ่งอยู่นานยิ่งมีความเสี่ยง
นอกจากนี้ JWT จะเก็บข้อมูลไว้ใน Token(By Value) ในขณะที่ API Key ไม่สามารถเก็บข้อมูลได้(By Reference) นั่นคือถ้าต้องการรู้ว่า API Key นี้ยังใช้งานได้อยู่หรือไม่ต้องไปถามคนที่ Generate API Key ขึ้นมา ในขณะที่ JWT เราสามารถแกะดูข้่อมูลใน body และ validate ได้แม้จะไม่ใช่คน Sign Token ก็ตาม
JWT vs Session
การใช้งาน JWT ต่างจากการใช้งาน Session ยังไง? นี่น่าจะเป็นคำถามที่หลายคนสงสัย สิ่งที่ทำให้ JWT แตกต่างจาก Session คือ
- JWT เป็น Stateless
- Session เป็น Stateful
แล้ว Stateless กับ Stateful แตกต่างกันยังไง ลองนึกถึงร้านอาหารและกาแฟ เวลาเราเก็บสะสมแต้มจะมีอยู่ 2 รูปแบบ คือ
-
ร้านเป็นคนเก็บข้อมูล เราแค่บอกเบอร์โทรศัพท์ของเราจะได้คะแนนจากการซื้อของครั้งนั้นทันที นั่นคือร้านค้า(Server) เป็นคนเก็บข้อมูลให้เรา แบบนี้เรียกว่า Stateful หมายถึง Server ทำการเก็บข้อมูลให้เรา(Session ก็จะเก็บข้อมูลไว้บน server)
-
ร้านไม่เก็บข้อมูลแต่ใช้การปั๊มตราลงในบัตรสะสมแต้มแล้วให้ลูกค้าเก็บไว้(Client) ทุกครั้งที่คุณจะสะสมแต้มหรือแลกของคุณต้องเอาบัตรสะสมแต้มมาด้วย นั่นคือ Stateless หมายถึง client เป็นคนเก็บข้อมูล(JWT จะเก็บอยุ่ที่ฝั่ง client ส่วนใหญ่อยู่ใน local storage)
แล้วทีนี้ JWT(Stateless) และ Session(Stateful) มีข้อดีและข้อเสียยังไง
ข้อดีของ Session(Stateful) คือ
- การจัดเก็บ Session อยู่ฝั่ง Server ดังนั้นจะปลอดภัยว่า
- เขียน Code สั้นกว่า ไม่ต้องทำอะไรเลย เพราะวิธีการทำนั้นตรงไปตรงมา ไม่มีทางเลือกอะไร
- การ revoke(ห้ามเข้าใช้งาน) ทำได้ง่าย
- Session ID ถูกเก็บอยู่ใน Cookie ซึ่งมี HttpOnly และ Secure Flag ในการป้องกัน Cross-Site Scripting(XSS)
- กำหนดช่วงเวลา Lifetime สั้นกว่า Session จะไม่ expire ตราบใดที่ user ยังใช้งานอยู่
ข้อเสียของ Session(Stateful)
- ยิ่งมีข้อมูลอยู่บน Session เยอะ ยิ่งทำให้ Server ทำงานหนัก
- Scale ได้ยาก เพราะทุกอย่างอยู่บน Server
ข้อดีของ JWT(Stateless)
- สามารถขยายตัวได้ง่าย(Scalability)
ข้อเสียของ JWT(Stateless)
- ต้องกำหนดให้ Token มีอายุการใช้งานยาวเพียงพอต่อการใช้งาน(ยิ่งอยู่นานยิ่งมีความเสี่ยง)
- ไม่มีมาตรฐานในการ revoke token(ยกเลิกการใช้งาน)
- ความปลอดภัยของ JWT ขึ้นอยู่กับ algorithm, ความยาวของ secret และ awareness ของ developer
ส่วนประกอบของ JWT
JWT นั้นแบ่งเนื้อหาออกเป็น 3 ส่วนด้วยกันตือ
-
Header ส่วนนี้จะบอก Algorithm ที่จะใช้ในการ Sign Token
{ "alg": "HS256", "typ": "JWT" }
ในบางกรณีที่เราจัดเก็บ Key ไว้ใน Key Manager เราจะเพิ่ม kid เข้าไป ซึ่ง kid ย่อมาจาก Key ID ซึ่งจะใช้ในการ query เพื่อนำ key ออกมาจาก Key Manager เพื่อเข้ารหัสหรือถอดรหัส
{ "alg": "HS256", "typ": "JWT", "kid": "d8cf3fa301a34c968502a7051bfdc0a8" }
-
Payload หรือ Claims เนื้อหาที่จะใส่เข้าไปใน Token ในส่วนของเนื้อหาจะมี
{ "sub": "1234567890", "name": "John Doe", "admin": true, "iat": 1516239022 }
ในมาตรฐาน JWT ได้กำหนด key ที่จะใส่ใน payload หรือ claims ซึ่งเราไม่จำเป็นต้องใส่ก้ได้ แต่ถ้าใส่ลงไปแล้วทุกๆ service ที่อ่าน JWT จะเข้าใจความหมายของ key เหล่านี้ทั้งหมด
- sub ย่อมาจาก subject หมายถึง token นี้ออกให้กับใครหรือสิ่งใด อาจเป็น user หรือ service ก็ได้่ส่วนใหญ่จะใช้ UUID หรือ GUID ในการ reference
- iss ย่อมาจาก issuer หมายถึง ใครเป็นคน Sign Token นี้ให้
- aud ย่อมาจาก audience หมายถึง ผู้ที่จะต้องรับ JWT นี้ไปใช้งาน
- iat ย่อมาจาก issue at Token นี้ถูก Sign เมื่อไหร่
- nbf ย่อมาจาก not before Token นี้จะยังใช้งานไม้ได้จนกว่าจะถึงเวลา(timestamp)นี้
- exp ย่อมาจาก expire token นี้จะหมดอายุเมื่อถึงเวลา(timestamp) นี้
- jti ย่อมาจาก JWT ID id ของ JWT ทำให้ JWT นี้สสามารถใช้ได้ครั้งเดียวไม่สามารถนกลับมาใช้ใหม่ได้ เอาไว้ป้องกันคนนำมาใช้ซ้ำ (replay attack)
ส่วนข้อมูลอื่นๆ คุณสามารถใส่เพิ่มได้ตามอัธยาศัย แต่สิ่งที่ต้องระวังคือ
JWT ใช้การ Base64 encode ซึ่งไม่สามารถเก็บความลับได้ห้ามใส่ข้อมูลที่เป็นความลับ(Sensitive Data) ลงไปใน Payload เด็ดขาด
-
Signature ส่วนนี้จะเป็นข้อความที่เข้ารหัสแล้ว ใช้สำหรับการยืนยันว่าข้อมูลที่อยู่ใน Payload นั้นไม่ถูกแก้ไข ซึ่งจะมีการเข้ารหัสตามที่เรากำหนดไว้ใน Header ลองดูวิธีการ Validate JWT ได้ในหัวข้อต่อไป
หลังจาก encode ข้อมูลทั้ง 3 ส่วนแล้วเราก็จำนำมารวมกันโดยเชื่อมทั้ง 3 เข้าหากันด้วยเครื่องหมาย .
Bas64Encode(Header).Bas64Encode(Payload).Bas64Encode(Signature)
JWT มีวิธีการตรวจสอบ(Validate)ยังไง?
Header ของ JWT เป็นส่วนที่บอกว่าเลือก algorithm ไหนมาใช้ Sign Token ซึ่งการ Sign จะทำได้ 2 วิธีคือ
-
Hashing เราจะใช้ Secret ตัวเดียว นั่นทำให้ server ที่ Sign Token และ Validate Token ต้องเป็นตัวเดียวกัน algorithm ที่เลือกใช้จะเป็น
- HS256
- HS384
- HS512
ทั้ง 3 ตัวนี้เป็น algorithm HMAC เหมือนกันแต่ความยาวของ Secret ต่างกัน(ตัวเลขข้างหลังหมายถึงความยาวของ secret หน่วยเป็น bit)
-
Encryption + Hashing เราจะเข้ารหัส Signature ด้วย Private Key(Sign Token) ส่วน Public Key จะใช้ตรวจสอบ(Validate) Token พอ Key ที่ใช้ในการ Sign กับ Validate เป็นคนละตัว เลยทำให้เราสามารถเลือกที่จะให้ Server ที่ทำหน้าที่ Sign และ Server ที่ทำหน้าที่ Validate เป็นคนละตัวได้
Algorithm ที่สามารถเลือกใช้ได้มี 3 กลุ่มด้วยกันคือ
- RS256, RS384, RS512 กลุ่มนี้จะ Encryption ด้วย RSA และ Hash ด้วย SHA256, SHA384 หรือ SHA512 ตามตัวเลขที่ต่อท้ายด้านหลัง เช่น RS512 จะมีขั้นตอนการเข้ารหัส ดังนี้
- ใช้ Private key encrypt payload ด้วย algorithm RSA
- หลังจากนั้นจะนำ cipher text(ผลลัพธ์ที่ได้จากการเข้ารหัส) ไป hash ด้วย SHA512
- ES256, ES384, ES512 กลุ่มนี้จะ
- Encryption ด้วย ECDSA(ความปลอดภัยสูงกว่า RSA)
- Hash ด้วย SHA256, SHA384 หรือ SHA512 ตามตัวเลขที่ต่อท้ายด้านหลัง
- PS256, PS384, PS512 กลุ่มนี้จะ
- Encryption ด้วย RSAPSS(เป็น RSA ที่กำหนด Version ที่จะใช้ในการ Sign)
- Hash ด้วย SHA256, SHA384 หรือ SHA512 ตามตัวเลขที่ต่อท้ายด้านหลัง
ข้อควรระวังคือห้ามใส่ “alg”: none เพราะจะหมายถึงเราไม่มีการใช้ Signature นั่นก็คือใครอยากแก้อะไรใน Payload ก็ทำได้เลยตามสบาย
- RS256, RS384, RS512 กลุ่มนี้จะ Encryption ด้วย RSA และ Hash ด้วย SHA256, SHA384 หรือ SHA512 ตามตัวเลขที่ต่อท้ายด้านหลัง เช่น RS512 จะมีขั้นตอนการเข้ารหัส ดังนี้
ทำไมเราไม่ควรใช้ HS256, HS384 และ HS512
เราควรเลือกใช้ RS512 แทน HS256, HS384 และ HS512 เพราะเราสามารถ Brute force หา Secret ที่เราใช้ในการทำ Hashing ด้วย jwt_cracker ซึ่งขั้นตอนของการค้นหา secret จะมีดังนี้
-
Install JWT Cracker
$ npm install jwt-cracker
-
Crack JWT Secret
$ jwt-cracker -t [token]
จำนวนอักขระยิ่งยาว ยิ่งใช้เวลานานในการ brute force ดังนั้นถ้าคุณยังต้องใช้ HS อยู่ แนะนำให้ใช้ HS512 และตั้ง Secret อย่างน้อย 64 ตัวอักษร และ Secret ต้องมาจากกการ random เท่านั้นอย่าเอาชื่อ application หรือ project เข้ามาใส่โดยเด็ดขาด
ตัวอย่าง เราจะ Brute force JWT ด้วย jwt-cracker
$ jwt-cracker -t "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE"
ผลลัพธ์ที่ได้ออกมาตือ JWT Token นี้ Sign ด้วย algorithm HS256 และใช้ secret เป็น Sn1f
Attempts: 100000 Attempts: 200000 Attempts: 1600000 SECRET FOUND: Sn1f Time taken (sec): 21.733 Attempts: 1640000