บทความนี้ผมเขียนขึ้นมาเพราะ เห็นหลายๆ คน implement token กันผิดๆ จริงๆ อาจจะไม่ผิดก็ได้ แต่บางอย่างมันทำให้เอะใจ ว่า
การ design token แบบนี้ มันทำให้สูญเสียคุณสมบัติบางอย่างทางด้านความปลอดภัย (security)ไปรึเปล่า ?
ลองมาดูกันครับว่า คุณสมบัติของ token ที่ว่ามีอะไรกันบ้าง
ก่อนจะไปอ่านข้างล่าง สำหรับคนที่ยังไม่รู้ว่า OAuth 2.0 คืออะไร ลองหาข้อมูลอ่านกันก่อนน่ะครับ ซึ่งผมจะไม่ได้เขียนอธิบายไว้ในบทความนี้
โอเค งั้นมาเริ่มกันเลย!
เพราะถ้า token ซ้ำกัน อาจทำให้ user 2 คน ได้สิทธิ์ของอีกคนนึงไป (ถ้า token เหมือนกันแสดงว่าจะทำให้ได้สิทธิ์เดียวกันด้วยรึเปล่า ?)
token ที่ดีต้องห้ามได้ค่าที่ซ้ำกัน
สมมติว่า token นั้นมีวันหมดอายุแนบมากับ token ด้วย แต่ token ดันเปิดช่องโหว่ให้สามารถแก้ไขได้ จะเกิดอะไรขึ้น?
แล้วถ้า token นี้ถูกขโมยไปล่ะ แสดงว่าคนที่ขโมย token ไปก็สามารถใช้ token นี้ได้ตลอดชีพ?
ยิ่งถ้าดันเก็บ user id หรือสิทธิ์ (authority) ไว้ใน token แล้วสามารถแก้ได้ นั่นหมายความว่า เราสามารถเข้าถึงระบบด้วยสิทธิ์ หรือบัญชีผู้ใช้ของใครก็ได้
token ที่ดี ต้องห้ามแก้ไขได้ (ต่อให้แก้ไขได้ ก็ต้องไม่ valid)
อาจจะ encrypt หรือ hash ค่าแล้วเอามา compare กันก็ได้
อ้อ! ตรงนี้สำคัญครับ การ compare token แนะนำให้ป้องกัน Timing Attacks ด้วย
คือให้ใช้วิธีการ compare string ที่มัน secure กว่าการ == หรือ .equals ลองอ่านบทความนี้ดู
เมื่อป้องกันการแก้ไขแล้ว แต่ token นั้นดันมีอายุนานมากๆ เช่น 1 เดือน 3 เดือน 6 เดือน หรือ 1 ปี จะเกิดอะไรขึ้นถ้า token นั้นถูกขโมยไป?
คนที่ได้ token ไปก็จะสามารถใช้งาน token ด้วยสิทธิ์ของเราไปได้ตลอด จนกว่า token นั้นจะหมดอายุ
อย่าใช้วิธี generate token ที่ง่ายเกินไป จนทำให้สามารถคาดเดาค่า token ได้ ให้คิดว่ามีโอกาสเป็นไปได้เสมอที่อาจจะมีคนฟลุกเดา token ในระบบเราได้
เช่น ถ้าใช้ uuid เป็น token แน่นอนว่า uuid มันยากมากที่จะสุ่มเดาถูก แต่ถ้าระบบเรามีคนใช้งานเยอะเป็นหมื่น เป็นแสน เป็นล้าน หรือหลายล้านคน มันก็มีโอกาส ต่อให้เป็น 0.000001 % มันก็คือโอกาสที่จะสามารถเดาได้
อ้างอิง uuid ขนาด 128 bit length
จะใช้ uuid ก็ได้ แต่ถ้าสุ่มได้ค่าเดิมให้สุ่มใหม่ และควรมีการทำ hashing เข้าไปด้วย เพื่อป้องกันการแก้ไข และการ hash ให้เพิ่ม private key และ secret key
เข้าไปด้วย โดย
เพื่อให้ค่า hash ที่ออกมาไม่ได้ค่าเดิมเหมือนกันทุกครั้ง ทำให้ยากต่อการสุ่มเดา และกรณีที่ token ถูกขโมย ถ้าเราสั่งเปลี่ยน private key ก็จะทำให้ token ทั้งหมด invalid ตามไปด้วย
ในกรณีที่ฟลุ๊กสุ่มได้ค่าเดียวกันจริงๆ (ถ้าระบบไม่ได้กันไว้) secret key นี้แหล่ะที่จะป้องกันได้ครับ
ผมอธิบายไว้ข้างล่างแล้ว
เพราะถ้ามีการเปลี่ยน (revoke) token แล้วยังสามารถใช้งาน token เก่าได้ จะเกิดอะไรขึ้น ถ้า token เก่าถูกขโมยไป นั่นหมายความว่าคนที่ได้ token นั้นไป ก็จะมีสิทธิ์เทียบเท่ากับเราซึ่งเป็นเจ้าของ token จริงๆ
token ควรเป็น stateful ครับ คือ แนะนำให้เก็บลง database (ในทางปฏิบัติอาจจะเก็บลง redis (key/value data store) หรืออะไรที่มัน read/write ได้เร็วๆ) ต่อให้เราตั้งวันหมดอายุให้ token แต่ถ้า token นั้นมันยัง valid อยู่ มันก็จะใช้งานได้ตลอดไปจนกว่ามันจะหมดอายุ
case นี้ access token จะไม่ค่อยเห็นภาพสักเท่าไหร่ เพราะอายุมันสั้น ลองนึกถึงว่า ถ้า refresh token ถูกขโมยไปล่ะ จะเกิดอะไรขึ้น?
เพราะฉะนั้น แนวทางปฏิบัติที่ดี เมื่อเลิกใช้ token นึงแล้วควร revoke token นั้นทิ้งไปเลยครับ
(เพื่อกันคนอื่นเอาไปใช้งานต่อ)
ทั้ง access token และ refresh token แนะนำให้ใช้งานได้ ครั้งต่อครั้ง
ถ้ามีการขอ token ใหม่ token เดิมต้อง invalidate (revoke)ไปทันที
กรณีเลวร้าย token ของ users ในระบบเราถูกขโมยไปจำนวนมาก เพราะระบบเรามีช่องโหว่ รูรั่ว บางจุด เราก็ควรจะมีช่องทางให้สามารถเพิกถอน token บางส่วน หรือทั้งหมดของระบบได้
และข้อดีอีกอย่างนึงของข้อนี้คือ มันทำ logout ได้ ซึ่งมันเป็นการ logout จริงๆ ไม่ใช่แค่การเพิกถอน token จาก Front-end เท่านั้น
ถ้าเพิกถอน refresh token หรือ regenerate refresh token แล้ว ให้ revoke คู่ access token มันทิ้งไปด้วย
อย่าเอาข้อมูลสำคัญ เช่น password หรือข้อมูลสำคัญทางการเงิน มาเก็บไว้ใน token เด็ดขาด เพราะถ้าโดนขโมย token ไปนี่จบเลย
เพราะถ้ามีโอกาส hash หรือ decrypt ออกมาได้ นั่นหมายความว่า เราสามารถที่จะสร้าง token ขึ้นมาใช้งานเองได้ โดยไม่ผ่านระบบ และการที่สร้าง token ขึ้นมาเองได้ แสดงว่าจะใส่สิทธิ์อะไรลงไปก็ได้
ข้อนี้จะคล้ายๆ กับข้อ 6 แต่ไม่เหมือนกันซ่ะทีเดียว คุณเคยได้ยินประโยคนี้มั้ย
ถ้าบัญชี Facebook หรือ Twitter โดน Hack ให้ทำการเปลี่ยน password ซ่ะ
เมื่อทำตามที่แนะนำก็ปรากฏว่าบัญชีนั้นกลับมาเป็นของเราเหมือนเดิม
แล้วเค้าทำยังไง?
password มีความเกี่ยวข้องอะไรกับ token รึเปล่า ทำไมเปลี่ยน password แล้วได้บัญชีกลับคืน ?
แน่นอนครับ คือ เราจะเอา password เราเข้าไป hash ค่ารวมเป็นส่วนหนึ่งใน token ด้วย
(แต่เป็น password ที่ถูก hash มาแล้วน่ะ ไม่ใช่ raw password)
เพราะถ้า เราเปลี่ยน password ก็จะทำให้ค่า hash ที่ได้ใหม่ กับค่า hash เดิมที่เคยอยู่ใน token นั้นไม่ตรงกัน ทำให้ token นั้นๆ invalid ตามไปด้วย token ที่ถูกขโมยไปก็จะถูกเพิกถอนไปเองโดยอัตโนมัติ
ถ้าใครเขียน Java Spring Framework ลอง search คำว่า Hash-Based Token
ดูครับว่าเค้าทำกันยังไง ซึ่งในตัวอย่างจะเป็นการใช้ใน remember me แต่เราจะเอามาประยุกต์ใช้ในส่วน refresh token ครับ
และ Spring fixed hashing algorithm ไว้เป็น MD5 แต่เราจะ implement ใหม่เป็นเป็น hashing อย่างอื่นแทน ซึ่งอาจใช้ SHA384 หรือ hash algorithm อื่นๆ ที่สูงกว่าก็ได้ (เช่น HMACSHA384) โดยยกเว้น hashing ดังต่อไปนี้
อ้างอิง https://crackstation.net/hashing-security.htm
จะใช้ hashing อะไรให้ดูตามนี้ครับ สีแดงคือไม่ปลอดภัย
token ที่ดีควรจำกัดสิทธิ์การเข้าถึงได้ คือเมื่อเอา token ดังกล่าวไปใช้ ระบบต้องรู้ว่า token นั้นมีสิทธิ์แค่ไหนและทำได้แค่ไหน
ตามความคิดเห็นส่วนตัว ไม่ควรเอาสิทธิ์ (role หรือ authority) เก็บไว้ใน token ครับ เพราะจะทำให้แก้ไขสิทธิ์แบบ realtime ไม่ได้ ควรจะเอาสิทธิ์ไว้ใน database หรือ cache ไว้ใน redis แล้วไปอ่านค่ามาจาก API แทน
แต่ถ้าเป็นระบบที่ไม่ได้เคร่ง หรือซีเรียสอะไรมาก จะเก็บไว้ใน token ก็ได้ แต่ผมไม่แนะนำครับ จากเหตุผลด้านบน (เวลาใช้งานไป จะเจอปัญหาคือแก้สิทธิ์ user แบบ real time ไม่ได้)
ถ้าเรา design ระบบที่เป็น Stateless และใช้ Token-Based Authentication แทน Session สิ่งหนึ่งที่ต้องกังวล คือ คอขวดส่วนนึงมันจะมาอยู่ที่ระบบ authentication ของเรา เพราะฉะนั้นก่อนจะติดสันใจใช้ encryption หรือ hashing ตัวใดในการเอาเข้ามาช่วยในการ sign token แล้ว ให้คิดถึงความเหมาะสมด้วยครับ ว่ามันเหมาะมั้ย จะทำให้ระบบมันช้าไปรึเปล่า
encryption บางอย่างมันปลอดภัยจริงๆ แต่มันทำให้ระบบช้ามากๆ มันคุ้มมั้ยที่เราจะเอามาใช้ กับถ้าเราเลือก encryption ขนาดกลางๆ แต่ใช้วิธีเปลี่ยน key หรือมีอย่างอื่นเสริมเข้ามาเพื่อทำให้มัน secure ขึ้น มันจะดีกว่ามั้ย อันนี้ต้องช่างน้ำหนักกันเอาเองน่ะครับ
การ implement OAuth 2.0 คุณเคยรู้มั้ยว่าตอนที่ Grant authorize (กรณี response_type=code) มันมี parameter ชื่อ state
ด้วย
parameter ตัวนี้ใช้สำหรับป้องกัน CSRF (Cross Site Request Forgery) ดังนั้นเพื่อความปลอดภัยมากยิ่งขึ้น ให้กำหนดค่าให้มันด้วยเสมอ ลองอ่านจาก spec มันดูครับ
น่าจะเป็นคำถามที่หลายๆ คนอยากรู้
สำหรับผมมันเหมาะกับการเอามาใช้เพื่อป้องกันการแก้ไขข้อมูลครับ เพราะมันเป็น digital signature ดีๆ นี่เอง
แต่ผมเห็นหลายๆ คนเอา jwt มาใช้ แค่ sign/verify token ธรรมดาๆ แล้วก็ยอมรับว่า token มัน valid แต่ไม่ได้มีการ check ว่า token นั้นถูก revoke ทิ้งไปแล้วหรือยัง ซึ่งการทำแบบนี้ ทำให้เป็นผลเสียมากกว่าผลดีครับ คือใช้ jwt แล้วก็ควรมีการ sync token กับ database หรือ data store หลังบ้านด้วย เพื่อ check ว่ามันถูก revoke ไปแล้วรึเปล่า
เพราะด้วย คุณสมบัติของ jwt เอง มันจะยัง valid เสมอจนกว่าจะ expires ไป ทำให้หลายๆ คนคิดว่าแค่นี้มันก็โอเคแล้ว
ถ้าแค่ sign/verify jwt token ก็ยังไม่พอที่จะเอามาใช้งานครับ เพราะทำให้ขาดคุณสมบัติข้อ 5. (ใช้ได้ครั้งเดียว) และข้อ 6. (ต้องสามารถเพิกถอน (Revoke) จาก Back-end ได้)
ลองอ่านเหตุผลนี้ดูครับ
Part 1
Part 2
ถ้าจะใช้ jwt ด้วยคุณสมบัติแค่ sign/verify token ธรรมดาๆ โดย ไม่ sync ลง database มันควรเป็นการใช้งาน token ที่ระยะเวลาสั้นๆ คือใช้แล้วทิ้ง อันนี้จะเหมาะกว่าครับ เช่นเอามาใช้กับ authorization_code ตอน grant authorize (เพราะใช้ในระยะเวลาสั้นๆ และใช้แล้วทิ้งเลย)
ถ้าทำตามด้านบนทั้งหมดที่ผมเขียนไว้ โดยใช้ jwt กับคุณสมบัติข้อ 2. (ป้องกันการแก้ไขได้) ก็ถือว่าดีครับ เพราะทำให้มัน ปลอดภัยจริงๆ
แต่ผมก็ยังไม่ได้ใช้ jwt เป็น access token & refresh token เนื่องจากมันต้องเขียน logic อะไรเพิ่มเติมไปอีกมาก ถ้าต้องการคุณสมบัติทุกข้อตามที่กล่าวมาครับ เลยไม่ได้ใช้ (เคยลองใช้แล้วแต่เปลี่ยนใจ)
ถ้าจะใช้ JWT มันต้องเหมาะสมกับงานนั้นๆ จริงๆ ผมถึงจะใช้
ลองอ่านบทความนี้ดูครับ ว่าผมใช้ jwt ในส่วนไหน
ผมใช้การ generate token ที่ชื่อว่า Hash-Based Token ครับ หน้าตาของ token จะเป็นแบบนี้
token = base64(token_id + ":" + expires + ":" + signature)
จริงๆ มันก็คล้ายๆ กับ jwt แหล่ะ เพียงแต่ว่าตรง signature เรา custom ค่า hash เอง ซึ่ง jwt จะทำไม่ได้ เพราะ jwt จะใช้เฉพาะ data ที่อยู่ใน header + payload เท่านั้นมาหาค่า signature
แต่ signature เราจะเท่ากับ
signature = hash_hex(token_id + ":" + expires + ":" + encrypted_password + ":" + private_key)
ตรง private_key ไม่ต้องมีก็ได้ ถ้า hashing ที่เราใช้เป็น HMAC เพราะ private_key จะอยู่ที่ HMAC อยู่แล้ว
ตรง encrypted_password ทำให้ผู้ใช้ สามารถ revoke token ได้เอง โดยการเปลี่ยน password ตามข้อ 9. (เมื่อ Token ถูกขโมย ต้องมีวิธีที่สามารถสั่งเพิกถอน token นั้นทิ้งได้)
ลองดู Code จากตัวอย่างนี้ (ภาษา Java)
เพื่อความปลอดภัยยิ่งขึ้น ทุกครั้งที่ generate token ให้ random Secret Key (One-Time Secret) ต่อ token นั้นขึ้นมา แล้วเอาไป hash รวมกับข้อมูลที่จะใช้ generate token ด้วย
คือมันทำให้ปลอดภัยยิ่งขึ้น และ สมมติถ้าเกิดว่าระบบ random token (ด้วย uuid) ขึ้นมาเป็นค่าเดียวกัน จะทำให้มี token แค่อันเดียวเท่านั้นที่ valid เพราะค่า Secret Key นี้
(hash ใครถูกต้อง คนนั้นก็ถือว่า valid ไป)
Random Token
token_id=d93d9672-e3a4-11e7-80c1-9a214cf093ae
secret_key=D6aQskgf
หน้าตาของ token
ZTY3MDU4OWYtY2E2Zi00M2U0LTgwMmEtZWZlODliNjg1YzQ2OjE1MTI2NDEwMjg2NDM6M2ExMGQwN2JjMGQ2OTE1N2Y5ZmZjMGNkYTc4OWMzNzNhZjIwYjlkZGNkNWZlOTU1MWVhYzMwMDhhZDA0MDI2MTZhYjNmNmExYzI1M2VhNDRiNmUzYjgyY2ZjMmM1ZjYy
เมื่อ decode ด้วย base64 จะได้เป็น
e670589f-ca6f-43e4-802a-efe89b685c46:1512641028643:3a10d07bc0d69157f9ffc0cda789c373af20b9ddcd5fe9551eac3008ad0402616ab3f6a1c253ea44b6e3b82cfc2c5f62
Generate Token
Verify Token
การออกแบบ token จริงๆ แล้วมันไม่ได้มีอะไรตายตัวครับ ว่าจะต้องเป็น 1 2 3 แบบนี้ๆ มันขึ้นอยู่กับความเหมาะสมของการใช้งานในแต่ละประเภทมากกว่า ว่าเราจะใช้ token นี้ทำอะไร ใน scenario ไหน การออกแบบ token ก็ควรที่จะเหมาะสมกับ scenario นั้นๆ
เหมือนอย่างที่เห็นหลายๆ คนใช้ jwt ในการทำ access_token ซึ่งจริงๆ แล้ว jwt ถือเป็นเครื่องมือที่ดี เหมาะกับการนำเอามาใช้ทำอะไรหลายๆ อย่าง แต่เมื่อลองคิดหลายๆ ตลบดูแล้ว ก็เจอข้อจำกัดของมันอยู่หลายจุด ที่จะเอามาใช้เป็น access_token & refresh token (ให้ดีจริงๆ) โดยประสบการณ์ ผมเองก็อยู่กับ jwt มาประมาณ 2 ปีกว่า เกือบ 3 ปีได้ ใช้มันจนชิน จนเห็นว่ามันยังไม่ใช่ (ยังไม่ดีที่สุดใน case นี้) เลยเลือกที่จะเปลี่ยนเครื่องมือไปใช้ตัวอื่นที่มันเหมาะสมกว่าแทนครับ
อันนี้เป็นบทความ jwt ที่ผมเคยเขียนไว้ตั้งแต่งต้นปี 58 จำได้ว่าตอนนั้นการใช้งาน jwt ในไทยยังไม่ได้เป็นที่แพร่หลายเหมือนในตอนนี้
เครื่องมือทุกเครื่องมือจะมีทั้งข้อดีและข้อเสียอยู่ในตัวของมัน ถ้าเราเลือกใช้ให้ถูกจุด ให้เหมาะสมกับงานหรือสถานการณ์ เราก็จะสามารถดึงประสิทธิภาพของมันออกมาได้อย่างเต็มที่
คิดว่าน่าจะมีประโยชน์ และน่าจะเป็นแนวทางที่ดีให้คนที่กำลังหาข้อมูลเกี่ยวกับเรื่องนี้อยู่น่ะครับ
ว่างๆ หลังเสร็จงานในแต่ละวัน ผมเอาเวลาทั้งหมดไปศึกษา OAuth2 + Web Security อย่างจริงจัง ถ้าใครสนใจ ก็มา Join กันได้ครับ
ผมเขียน Java Spring boot ไว้ที่
เป็นบทความที่ถูกย้ายมาจาก https://medium.com/@jittagornp/best-practice-ในการทำ-oauth2-access-token-refresh-token-457ae3bee4b7 ซึ่งผู้เขียน เขียนไว้เมื่อ Dec 6, 2017