
ในโลกดิจิทัลที่ขับเคลื่อนด้วยแอปพลิเคชันและบริการออนไลน์มากมาย ความปลอดภัยและการจัดการสิทธิ์การเข้าถึงข้อมูลของผู้ใช้งานกลายเป็นหัวใจสำคัญที่ไม่อาจมองข้ามได้เลยครับ ไม่ว่าจะเป็นการเข้าสู่ระบบเว็บไซต์, การเชื่อมต่อแอปพลิเคชันเข้ากับบริการของบุคคลที่สาม, หรือการเรียกใช้ API ต่างๆ กลไกเบื้องหลังที่ช่วยให้สิ่งเหล่านี้เกิดขึ้นได้อย่างปลอดภัยและมีประสิทธิภาพคือสิ่งที่นักพัฒนาและผู้ดูแลระบบทุกคนควรทำความเข้าใจอย่างลึกซึ้ง และในบทความนี้ เราจะพาคุณดำดิ่งลงไปในสองมาตรฐานสำคัญที่ทำงานร่วมกันได้อย่างทรงพลัง นั่นคือ OAuth 2.0 และ JWT Authentication ครับ
บทความนี้จะครอบคลุมตั้งแต่พื้นฐานไปจนถึงรายละเอียดเชิงลึก พร้อมตัวอย่างการนำไปใช้งานจริง เพื่อให้คุณเห็นภาพรวมและสามารถนำความรู้เหล่านี้ไปปรับใช้กับโปรเจกต์ของคุณได้อย่างมั่นใจ ไม่ว่าคุณจะเป็นนักพัฒนาที่กำลังมองหาระบบยืนยันตัวตนและการมอบสิทธิ์ที่แข็งแกร่ง หรือผู้ที่สนใจในเทคโนโลยีความปลอดภัยบนเว็บ บทความนี้คือคู่มือครบถ้วนที่คุณกำลังมองหาอยู่ครับ
สารบัญ
- การทำความเข้าใจพื้นฐานของ Authentication และ Authorization
- เจาะลึก OAuth 2.0: มาตรฐานการมอบสิทธิ์ (Delegated Authorization)
- OAuth 2.0 คืออะไร?
- บทบาทของผู้ที่เกี่ยวข้องใน OAuth 2.0
- Grant Types หรือ Flow ต่างๆ ของ OAuth 2.0
- Authorization Code Grant
- Client Credentials Grant
- Implicit Grant (ไม่แนะนำและเลิกใช้แล้ว)
- Resource Owner Password Credentials Grant (ควรหลีกเลี่ยง)
- Proof Key for Code Exchange (PKCE)
- กระบวนการทำงานของ OAuth 2.0 (Authorization Code Grant + PKCE)
- ข้อดีและข้อจำกัดของ OAuth 2.0
- เจาะลึก JWT (JSON Web Tokens): โทเคนสำหรับข้อมูลที่เชื่อถือได้
- JWT คืออะไร?
- ส่วนประกอบของ JWT
- ประเภทของ JWT: JWS และ JWE
- JWT ทำงานอย่างไรในการ Authentication
- ข้อดีและข้อจำกัดของ JWT
- การทำงานร่วมกัน: OAuth 2.0 และ JWT Authentication
- การนำไปใช้งานจริง: ตัวอย่าง Code Snippets (Node.js/Express)
- ตั้งค่าโปรเจกต์ Node.js/Express เบื้องต้น
- การสร้าง JWT (บน Authorization Server หรือ API Server)
- การยืนยัน JWT (บน Resource Server)
- ตัวอย่างการเรียกใช้ API ด้วย Access Token (Client-side Conceptual)
- ข้อควรพิจารณาด้านความปลอดภัยและการจัดการ
- การจัดการ Refresh Token
- การจัดเก็บ Access Token
- การเพิกถอนโทเคน (Token Revocation)
- การป้องกัน Replay Attacks
- การกำหนด Scope ที่เหมาะสม
- ตารางเปรียบเทียบ: Session-based vs. Token-based Authentication
- คำถามที่พบบ่อย (FAQ)
- สรุปและก้าวต่อไป
การทำความเข้าใจพื้นฐานของ Authentication และ Authorization
ก่อนที่เราจะก้าวเข้าสู่โลกของ OAuth 2.0 และ JWT อย่างเต็มตัว เรามาทำความเข้าใจแนวคิดพื้นฐานสองอย่างที่สำคัญและมักจะถูกเข้าใจผิดบ่อยๆ กันก่อนนะครับ นั่นคือ Authentication และ Authorization
Authentication (การยืนยันตัวตน) vs. Authorization (การอนุญาตสิทธิ์)
- Authentication (การยืนยันตัวตน): กระบวนการนี้คือการตรวจสอบว่า “คุณคือใคร?” ครับ เป็นการพิสูจน์ยืนยันว่าผู้ใช้งานหรือระบบที่พยายามเข้าถึงทรัพยากรนั้น เป็นตัวตนที่อ้างถึงจริง โดยทั่วไปจะทำผ่านชื่อผู้ใช้และรหัสผ่าน, ลายนิ้วมือ, การสแกนใบหน้า, หรือ One-Time Password (OTP) เป็นต้น ผลลัพธ์ของการยืนยันตัวตนคือระบบจะรู้ว่า “นี่คือผู้ใช้งาน A” หรือ “นี่คือแอปพลิเคชัน X” ครับ
- Authorization (การอนุญาตสิทธิ์): เมื่อระบบรู้แล้วว่า “คุณคือใคร” ขั้นตอนต่อไปคือการตัดสินใจว่า “คุณมีสิทธิ์ทำอะไรได้บ้าง?” ครับ การอนุญาตสิทธิ์คือการกำหนดสิทธิ์การเข้าถึงทรัพยากรหรือการกระทำต่างๆ ให้กับผู้ใช้งานหรือระบบที่ผ่านการยืนยันตัวตนแล้ว เช่น ผู้ใช้งาน A อาจมีสิทธิ์อ่านข้อมูลแต่ไม่มีสิทธิ์แก้ไข ส่วนผู้ใช้งาน B อาจมีสิทธิ์ทั้งอ่านและแก้ไขข้อมูลครับ
สรุปง่ายๆ คือ Authentication คือการเข้าประตู ส่วน Authorization คือการดูว่าคุณมีกุญแจสำหรับห้องไหนบ้างเมื่อเข้ามาในบ้านแล้วนั่นเองครับ
ทำไมต้องมีระบบที่ซับซ้อนขึ้น?
ในอดีต การยืนยันตัวตนและการอนุญาตสิทธิ์มักจะทำในระบบเดียว เช่น เว็บไซต์มีระบบล็อกอินของตัวเอง และจัดการสิทธิ์ภายในฐานข้อมูลของเว็บไซต์นั้นๆ แต่ในปัจจุบัน โลกของแอปพลิเคชันมีการเชื่อมโยงกันมากขึ้น:
- แอปพลิเคชันกระจายตัว: ผู้ใช้งานต้องการใช้บริการจากหลายแพลตฟอร์ม (เว็บ, โมบายล์, เดสก์ท็อป) และแอปพลิเคชันหนึ่งอาจต้องเข้าถึงข้อมูลจากอีกแอปพลิเคชันหนึ่ง (เช่น แอปแต่งรูปต้องการเข้าถึงรูปภาพจาก Google Photos)
- ความต้องการ Single Sign-On (SSO): ผู้ใช้งานไม่ต้องการจำรหัสผ่านมากมายสำหรับทุกบริการ การใช้บัญชี Google หรือ Facebook เพื่อล็อกอินบริการอื่นๆ เป็นตัวอย่างที่ดีของ SSO ครับ
- การมอบสิทธิ์อย่างปลอดภัย: แอปพลิเคชันหนึ่งไม่ควรต้องรู้รหัสผ่านของผู้ใช้งานเพื่อเข้าถึงข้อมูลในบริการอื่น แต่ควรได้รับ “สิทธิ์ชั่วคราว” ในการเข้าถึงข้อมูลบางอย่างเท่านั้น
- API-first architecture: บริการต่างๆ ถูกสร้างขึ้นมาโดยมี API เป็นแกนหลัก ซึ่งต้องการกลไกการยืนยันตัวตนและการอนุญาตสิทธิ์ที่ยืดหยุ่นและปลอดภัยสำหรับ API Calls ครับ
ด้วยเหตุผลเหล่านี้เอง มาตรฐานอย่าง OAuth 2.0 และ JWT จึงเข้ามามีบทบาทสำคัญในการแก้ไขปัญหาและตอบสนองความต้องการด้านความปลอดภัยที่ซับซ้อนเหล่านี้ครับ
เจาะลึก OAuth 2.0: มาตรฐานการมอบสิทธิ์ (Delegated Authorization)
OAuth 2.0 คืออะไร?
OAuth 2.0 ย่อมาจาก Open Authorization 2.0 เป็นมาตรฐานอุตสาหกรรมที่ออกแบบมาเพื่อ การมอบสิทธิ์ (Delegated Authorization) โดยเฉพาะครับ สิ่งสำคัญที่ต้องจำคือ OAuth 2.0 ไม่ใช่มาตรฐานสำหรับการยืนยันตัวตน (Authentication) โดยตรง แต่เป็นวิธีการที่ช่วยให้แอปพลิเคชันหนึ่ง (เรียกว่า Client) สามารถเข้าถึงทรัพยากรของผู้ใช้งาน (Resource Owner) ที่จัดเก็บอยู่ในบริการอื่น (Resource Server) โดยไม่ต้องรู้รหัสผ่านของผู้ใช้งานครับ
ลองนึกภาพว่าคุณต้องการให้แอปพลิเคชันแต่งรูปภาพบนมือถือของคุณสามารถเข้าถึงรูปภาพที่คุณเก็บไว้ใน Google Photos ได้ แทนที่คุณจะต้องกรอกชื่อผู้ใช้และรหัสผ่าน Google ของคุณในแอปแต่งรูป (ซึ่งเป็นอันตรายอย่างยิ่ง) OAuth 2.0 จะเข้ามาช่วยให้คุณสามารถ “มอบสิทธิ์” ให้แอปแต่งรูปนั้นเข้าถึง Google Photos ในนามของคุณได้ โดยที่คุณยังคงเป็นเจ้าของบัญชีและควบคุมสิทธิ์ได้เต็มที่ครับ
บทบาทของผู้ที่เกี่ยวข้องใน OAuth 2.0
ในการทำงานของ OAuth 2.0 จะมีผู้เกี่ยวข้องหลักๆ 4 ส่วนด้วยกันครับ:
- Resource Owner (เจ้าของทรัพยากร): คือผู้ใช้งานทั่วไปอย่างเราๆ ท่านๆ นี่แหละครับ ที่เป็นเจ้าของข้อมูลหรือทรัพยากรที่ต้องการให้แอปพลิเคชันอื่นเข้าถึง เช่น รูปภาพใน Google Photos, รายชื่อติดต่อใน Gmail, โพสต์บน Facebook เป็นต้น
- Client (ไคลเอนต์): คือแอปพลิเคชันที่ต้องการเข้าถึงทรัพยากรของ Resource Owner ครับ อาจจะเป็นแอปพลิเคชันบนเว็บ, บนมือถือ, หรือแอปพลิเคชันเดสก์ท็อปก็ได้ ตัวอย่างเช่น แอปแต่งรูปภาพ, เว็บไซต์ที่ต้องการดึงข้อมูลโปรไฟล์จาก Facebook
- Authorization Server (เซิร์ฟเวอร์ผู้ให้สิทธิ์): เป็นเซิร์ฟเวอร์ที่ทำหน้าที่ยืนยันตัวตนของ Resource Owner และจัดการการอนุญาตสิทธิ์ครับ เมื่อ Resource Owner ยินยอมให้ Client เข้าถึงทรัพยากรใดๆ Authorization Server จะออก Access Token ให้กับ Client
- Resource Server (เซิร์ฟเวอร์ทรัพยากร): เป็นเซิร์ฟเวอร์ที่จัดเก็บข้อมูลหรือทรัพยากรของ Resource Owner และทำหน้าที่ตรวจสอบ Access Token ที่ Client ส่งมา เพื่อยืนยันว่า Client มีสิทธิ์เข้าถึงทรัพยากรนั้นๆ จริงหรือไม่ ตัวอย่างเช่น Google Photos API, Facebook Graph API ครับ
ผู้ให้บริการรายใหญ่ๆ เช่น Google, Facebook, Twitter, Microsoft ต่างก็มี Authorization Server และ Resource Server ของตนเองครับ
Grant Types หรือ Flow ต่างๆ ของ OAuth 2.0
OAuth 2.0 กำหนด “Grant Types” หรือ “Flows” หลายรูปแบบ เพื่อให้เหมาะกับการใช้งานในสถานการณ์และประเภทของ Client ที่แตกต่างกัน โดยแต่ละ Flow จะมีขั้นตอนการขอ Access Token ที่แตกต่างกันไปครับ
Authorization Code Grant
เป็น Grant Type ที่ได้รับความนิยมและปลอดภัยที่สุดสำหรับแอปพลิเคชันบนเว็บ (Web Applications) และแอปพลิเคชันฝั่งเซิร์ฟเวอร์ครับ
- กระบวนการ: Client จะเปลี่ยนเส้นทาง Resource Owner ไปยัง Authorization Server เพื่อขอการอนุญาต Resource Owner จะล็อกอินและให้ความยินยอม Authorization Server จะส่ง Authorization Code กลับมายัง Client (ผ่าน Redirect URI) จากนั้น Client จะนำ Authorization Code นี้ไปแลกเป็น Access Token ที่ Authorization Server โดยตรง (ผ่าน Back-channel communication) ซึ่งทำให้ Access Token ไม่ถูกเปิดเผยในเบราว์เซอร์ครับ
- ข้อดี: ปลอดภัยสูง เพราะ Access Token ไม่ได้ถูกส่งผ่าน URL หรือเบราว์เซอร์โดยตรง เหมาะสำหรับ Client ที่สามารถเก็บ
client_secretได้อย่างปลอดภัย
Client Credentials Grant
ใช้สำหรับสถานการณ์ที่แอปพลิเคชันต้องการเข้าถึงทรัพยากรของตัวเอง ไม่ใช่ทรัพยากรของผู้ใช้งาน หรือสำหรับการสื่อสารระหว่าง Server-to-Server ครับ
- กระบวนการ: Client ส่ง
client_idและclient_secretไปยัง Authorization Server โดยตรง เพื่อขอ Access Token โดยไม่ต้องมี Resource Owner เข้ามาเกี่ยวข้อง - ข้อดี: ง่ายและตรงไปตรงมา เหมาะสำหรับ Machine-to-Machine communication หรือ Client ที่ทำหน้าที่เป็นบริการของตนเอง
Implicit Grant (ไม่แนะนำและเลิกใช้แล้ว)
เคยถูกใช้สำหรับ Single-Page Applications (SPA) หรือแอปพลิเคชันบนมือถือที่ไม่มี Backend แต่ปัจจุบันไม่แนะนำให้ใช้แล้วเนื่องจากข้อจำกัดด้านความปลอดภัย
- กระบวนการ: Access Token จะถูกส่งกลับไปยัง Client โดยตรงผ่าน URL fragment หลังจาก Resource Owner ให้ความยินยอม
- ข้อจำกัด: Access Token ถูกเปิดเผยใน URL ทำให้เสี่ยงต่อการถูกดักจับและโจมตีด้วย Cross-Site Scripting (XSS)
Resource Owner Password Credentials Grant (ควรหลีกเลี่ยง)
ใช้ในกรณีที่ Client เป็นแอปพลิเคชันที่เชื่อถือได้มาก และต้องการเข้าถึงข้อมูลของผู้ใช้งานโดยตรง
- กระบวนการ: Client ขอชื่อผู้ใช้และรหัสผ่านจาก Resource Owner โดยตรง และส่งข้อมูลเหล่านั้นไปให้ Authorization Server เพื่อแลกเป็น Access Token
- ข้อจำกัด: Client ต้องจัดการกับชื่อผู้ใช้และรหัสผ่านของผู้ใช้งาน ซึ่งเป็นความเสี่ยงด้านความปลอดภัยอย่างมาก ควรหลีกเลี่ยงการใช้ Flow นี้และหันไปใช้ Authorization Code Grant พร้อม PKCE แทนครับ
Proof Key for Code Exchange (PKCE)
PKCE (มักอ่านว่า “Pixy”) ไม่ใช่ Grant Type ใหม่ แต่เป็นส่วนเสริมด้านความปลอดภัยสำหรับ Authorization Code Grant โดยเฉพาะสำหรับ Public Clients (เช่น Mobile Apps, SPAs) ที่ไม่สามารถเก็บ client_secret ได้อย่างปลอดภัย
- กระบวนการ: Client จะสร้าง
code_verifierแบบสุ่ม และแฮชมันเป็นcode_challengeก่อนที่จะเริ่มกระบวนการขอ Authorization Code จากนั้นเมื่อ Client ได้ Authorization Code กลับมา และพยายามแลกเป็น Access Token Client จะต้องส่งcode_verifierต้นฉบับกลับไปให้ Authorization Server เพื่อยืนยันว่า Client ที่ขอ Access Token คือ Client เดียวกับที่เริ่มกระบวนการขอ Authorization Code ในตอนแรก - ข้อดี: ป้องกัน Authorization Code จากการถูกดักจับและนำไปใช้โดย Client อื่นๆ ได้อย่างมีประสิทธิภาพ ทำให้ Authorization Code Grant ปลอดภัยสำหรับ Public Clients มากขึ้นครับ
กระบวนการทำงานของ OAuth 2.0 (Authorization Code Grant + PKCE)
เพื่อให้เห็นภาพชัดเจนขึ้น เรามาดูขั้นตอนการทำงานของ Authorization Code Grant ร่วมกับ PKCE ซึ่งเป็น Flow ที่แนะนำสำหรับเว็บแอปพลิเคชันและโมบายล์แอปพลิเคชันในปัจจุบันกันครับ
- Client เริ่มต้นการขอสิทธิ์:
- Client สร้าง
code_verifier(ค่าสุ่มที่ยาวและปลอดภัย) - Client สร้าง
code_challengeจากcode_verifier(โดยใช้ SHA256 และ Base64 URL-safe encoding) - Client เปลี่ยนเส้นทาง (redirect) Resource Owner ไปยัง Authorization Server พร้อมพารามิเตอร์ต่างๆ เช่น
client_id,redirect_uri,scope,response_type=code,code_challengeและcode_challenge_method=S256
- Client สร้าง
- Resource Owner ให้ความยินยอม:
- Authorization Server แสดงหน้าล็อกอินให้ Resource Owner (ถ้ายังไม่ได้ล็อกอิน)
- Authorization Server แสดงหน้าขอความยินยอม (Consent Screen) ให้ Resource Owner ตรวจสอบว่า Client ต้องการสิทธิ์อะไรบ้าง และให้ Resource Owner ตัดสินใจว่าจะอนุญาตหรือไม่
- หาก Resource Owner ยินยอม Authorization Server จะสร้าง
Authorization Code
- Authorization Server ส่ง Authorization Code กลับไปยัง Client:
- Authorization Server เปลี่ยนเส้นทาง Resource Owner กลับไปยัง
redirect_uriของ Client พร้อมแนบAuthorization Codeมาด้วย เช่นhttps://client.example.com/callback?code=AUTH_CODE_XYZ
- Authorization Server เปลี่ยนเส้นทาง Resource Owner กลับไปยัง
- Client แลกเปลี่ยน Authorization Code เป็น Access Token:
- Client ส่งคำขอ HTTP POST ไปยัง Authorization Server โดยตรง (Back-channel) พร้อมพารามิเตอร์
grant_type=authorization_code,code=AUTH_CODE_XYZ,redirect_uri,client_idและcode_verifier(ที่เก็บไว้ตั้งแต่ตอนแรก) - หมายเหตุ: หากเป็น Confidential Client (เช่น Web App ที่มี Backend) ก็จะส่ง
client_secretมาด้วย แต่สำหรับ Public Client (เช่น SPA, Mobile App) จะใช้ PKCE แทนclient_secretครับ
- Client ส่งคำขอ HTTP POST ไปยัง Authorization Server โดยตรง (Back-channel) พร้อมพารามิเตอร์
- Authorization Server ตรวจสอบและออกโทเคน:
- Authorization Server ตรวจสอบ
Authorization Code,redirect_uri,client_idและcode_verifier(โดยสร้างcode_challengeจากcode_verifierที่ได้รับมา แล้วเปรียบเทียบกับcode_challengeที่ส่งมาตั้งแต่ขั้นตอนที่ 1) - หากถูกต้อง Authorization Server จะออก
Access Token(และอาจจะมีRefresh Tokenและ/หรือID Tokenด้วย) และส่งกลับให้ Client ในรูปแบบ JSON
- Authorization Server ตรวจสอบ
- Client ใช้ Access Token เพื่อเข้าถึง Resource Server:
- Client นำ
Access Tokenที่ได้รับไปแนบในส่วนหัวของคำขอ HTTP (Authorization: Bearer ACCESS_TOKEN) เพื่อเรียกใช้ API บน Resource Server
- Client นำ
- Resource Server ตรวจสอบ Access Token:
- Resource Server ตรวจสอบความถูกต้องของ
Access Token(อาจจะโดยการสอบถาม Authorization Server หรือตรวจสอบด้วยตัวเองหากเป็น JWT) - หาก Access Token ถูกต้อง Resource Server จะให้บริการตามคำขอของ Client และส่งข้อมูลกลับไป
- Resource Server ตรวจสอบความถูกต้องของ
จะเห็นได้ว่า OAuth 2.0 เป็นกลไกที่ซับซ้อนแต่มีประสิทธิภาพในการจัดการการมอบสิทธิ์ได้อย่างปลอดภัย โดยเฉพาะอย่างยิ่งเมื่อใช้ร่วมกับ PKCE ครับ
ข้อดีและข้อจำกัดของ OAuth 2.0
ข้อดี:
- ความปลอดภัย: Resource Owner ไม่ต้องเปิดเผยรหัสผ่านให้กับ Client ทำให้ลดความเสี่ยงจากการโจมตีประเภท Phishing หรือการจัดเก็บรหัสผ่านที่ไม่ปลอดภัย
- การควบคุมสิทธิ์: Resource Owner สามารถควบคุมได้อย่างละเอียดว่า Client จะเข้าถึงข้อมูลประเภทใดได้บ้าง และสามารถเพิกถอนสิทธิ์ได้ทุกเมื่อ
- ความยืดหยุ่น: รองรับการใช้งานหลากหลายรูปแบบ (Web, Mobile, Desktop, Server-to-Server) ผ่าน Grant Types ต่างๆ
- มาตรฐานอุตสาหกรรม: เป็นมาตรฐานที่ได้รับการยอมรับและใช้งานอย่างแพร่หลาย ทำให้ง่ายต่อการบูรณาการกับบริการต่างๆ
ข้อจำกัด:
- ความซับซ้อน: การทำความเข้าใจและนำไปใช้งานอาจมีความซับซ้อน โดยเฉพาะสำหรับผู้เริ่มต้น
- ไม่ใช่ Authentication: OAuth 2.0 ไม่ได้ออกแบบมาเพื่อยืนยันตัวตนของผู้ใช้งานโดยตรง หากต้องการ Authentication ต้องใช้ร่วมกับมาตรฐานอื่น เช่น OpenID Connect (ซึ่งอยู่บน OAuth 2.0 อีกที)
- ความเสี่ยงด้านการกำหนดค่า: หากกำหนดค่าผิดพลาด (เช่น
redirect_uriไม่ถูกต้อง) อาจทำให้เกิดช่องโหว่ได้ครับ
สำหรับข้อมูลเชิงลึกเกี่ยวกับ OpenID Connect ที่ต่อยอดมาจาก OAuth 2.0 เพื่อให้สามารถยืนยันตัวตนได้ด้วย สามารถ อ่านเพิ่มเติม ได้ครับ
เจาะลึก JWT (JSON Web Tokens): โทเคนสำหรับข้อมูลที่เชื่อถือได้
JWT คืออะไร?
JWT ย่อมาจาก JSON Web Tokens เป็นมาตรฐาน (RFC 7519) ที่ใช้อธิบายวิธีการสร้างโทเคนแบบ Self-contained สำหรับการส่งข้อมูลอย่างปลอดภัยระหว่างผู้เกี่ยวข้องครับ ข้อมูลที่อยู่ใน JWT สามารถถูกตรวจสอบความถูกต้องและเชื่อถือได้ด้วยลายเซ็นดิจิทัล (Digital Signature) ครับ
ลองนึกภาพว่า JWT เป็นเหมือนพาสปอร์ตดิจิทัลที่ออกให้โดยเซิร์ฟเวอร์ มันมีข้อมูลประจำตัวและสิทธิ์ต่างๆ ของผู้ถือ ซึ่งเซิร์ฟเวอร์อื่นๆ ที่เชื่อถือผู้ที่ออกพาสปอร์ตนี้ สามารถตรวจสอบความถูกต้องของพาสปอร์ตได้ด้วยตัวเอง โดยไม่ต้องไปถามผู้ที่ออกพาสปอร์ตทุกครั้งครับ
JWT มักถูกใช้เป็น Access Token ใน OAuth 2.0 หรือเป็นโทเคนสำหรับการยืนยันตัวตนในระบบ API ที่เป็น Stateless (ไม่มีการเก็บสถานะ Session ไว้บน Server) ครับ
ส่วนประกอบของ JWT
JWT จะประกอบด้วย 3 ส่วนหลักๆ ที่คั่นด้วยจุด (.) และถูกเข้ารหัสด้วย Base64 URL-safe Encoding ครับ
ส่วนหัว.ส่วนข้อมูล.ลายเซ็น
Header.Payload.Signature
เรามาดูรายละเอียดแต่ละส่วนกันครับ
Header (ส่วนหัว)
ส่วนหัวเป็นออบเจกต์ JSON ที่ระบุข้อมูลเกี่ยวกับตัวโทเคนเอง โดยหลักๆ จะระบุ:
alg(Algorithm): อัลกอริทึมที่ใช้ในการสร้างลายเซ็น เช่น HS256 (HMAC SHA256) หรือ RS256 (RSA SHA256)typ(Type): ประเภทของโทเคน ซึ่งสำหรับ JWT มักจะเป็น “JWT”
ตัวอย่าง Header (JSON):
{
"alg": "HS256",
"typ": "JWT"
}
เมื่อนำไปเข้ารหัส Base64 URL-safe จะได้เป็นส่วนแรกของ JWT ครับ
Payload (ส่วนข้อมูล)
ส่วน Payload เป็นออบเจกต์ JSON ที่บรรจุ “Claims” ซึ่งเป็นข้อมูลที่เราต้องการส่งผ่านโทเคน Claims แบ่งออกได้เป็น 3 ประเภทครับ:
- Registered Claims: เป็น Claims มาตรฐานที่ถูกกำหนดโดย IANA เพื่อจุดประสงค์เฉพาะ แต่ก็เป็นทางเลือก (optional) ไม่ได้บังคับ ตัวอย่างเช่น:
iss(Issuer): ผู้ออกโทเคนsub(Subject): หัวข้อของโทเคน (มักจะเป็น ID ผู้ใช้งาน)aud(Audience): ผู้รับโทเคน (Client ID หรือชื่อบริการ)exp(Expiration Time): เวลาหมดอายุของโทเคน (เป็น Unix timestamp)nbf(Not Before): เวลาที่โทเคนจะเริ่มมีผลใช้งานiat(Issued At): เวลาที่โทเคนถูกออกjti(JWT ID): รหัสเฉพาะของ JWT
- Public Claims: เป็น Claims ที่เราสามารถกำหนดเองได้ แต่ควรระบุชื่อ Claim ที่ไม่ซ้ำกับ Registered Claims หรือ Standard Claims อื่นๆ เพื่อหลีกเลี่ยงความขัดแย้ง
- Private Claims: เป็น Claims ที่เรากำหนดเองเช่นกัน แต่ใช้สำหรับข้อตกลงระหว่างผู้ส่งและผู้รับเท่านั้น
ตัวอย่าง Payload (JSON):
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iss": "siamlancard.com",
"exp": 1678886400, // เวลาหมดอายุในรูปแบบ Unix timestamp
"scope": ["read:profile", "write:posts"]
}
เมื่อนำไปเข้ารหัส Base64 URL-safe จะได้เป็นส่วนที่สองของ JWT ครับ
Signature (ลายเซ็น)
ส่วนลายเซ็นคือหัวใจสำคัญที่ทำให้ JWT มีความน่าเชื่อถือครับ มันถูกสร้างขึ้นโดยใช้:
- ส่วน Header ที่เข้ารหัสแล้ว
- ส่วน Payload ที่เข้ารหัสแล้ว
- Secret Key (สำหรับอัลกอริทึมแบบ Symmetric เช่น HS256) หรือ Private Key (สำหรับอัลกอริทึมแบบ Asymmetric เช่น RS256)
- อัลกอริทึมที่ระบุใน Header (เช่น HMAC SHA256)
สูตรการสร้าง Signature:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret_key
)
ลายเซ็นนี้จะช่วยให้ผู้รับโทเคนสามารถตรวจสอบได้ว่า:
- โทเคนนี้ถูกออกโดยผู้ที่เชื่อถือได้จริง (เพราะมี Secret Key หรือ Private Key ที่ถูกต้อง)
- ข้อมูลใน Header และ Payload ไม่ได้ถูกแก้ไขระหว่างทาง
หากมีใครพยายามแก้ไขข้อมูลใน Header หรือ Payload แม้แต่นิดเดียว ลายเซ็นก็จะใช้ไม่ได้ทันทีครับ
ประเภทของ JWT: JWS และ JWE
จริงๆ แล้ว JWT มีสองประเภทหลักๆ ครับ
- JWS (JSON Web Signature): นี่คือ JWT ที่เราพูดถึงข้างต้นครับ คือการใช้ลายเซ็นเพื่อรับประกันความสมบูรณ์ของข้อมูล (Integrity) และยืนยันผู้ส่ง (Authenticity) ข้อมูลใน Payload ยังคงอ่านได้ (Plaintext)
- JWE (JSON Web Encryption): นอกจากจะมีการเซ็นลายเซ็นแล้ว JWE ยังมีการเข้ารหัส (Encryption) ข้อมูลใน Payload ด้วย ทำให้ข้อมูลในโทเคนไม่สามารถอ่านได้โดยผู้ที่ไม่ได้รับอนุญาต เหมาะสำหรับกรณีที่ต้องการเก็บข้อมูลที่ละเอียดอ่อนมากๆ ไว้ในโทเคน
สำหรับกรณีการยืนยันตัวตนและการมอบสิทธิ์ทั่วไป JWS ก็เพียงพอแล้วครับ และเป็นสิ่งที่มักจะหมายถึงเมื่อพูดถึง “JWT” ครับ
JWT ทำงานอย่างไรในการ Authentication
เมื่อใช้ JWT ในการยืนยันตัวตน มันจะทำงานในลักษณะที่เรียกว่า Stateless Authentication ซึ่งต่างจาก Session-based Authentication ที่เซิร์ฟเวอร์ต้องเก็บ Session ID และสถานะของผู้ใช้งานไว้
- ล็อกอิน: ผู้ใช้งานส่งชื่อผู้ใช้และรหัสผ่านไปยัง Authentication Server
- สร้าง JWT: Authentication Server ตรวจสอบข้อมูลล็อกอิน หากถูกต้อง จะสร้าง JWT โดยใส่ข้อมูลผู้ใช้งาน (เช่น User ID, Role, Permission) ลงใน Payload และเซ็นด้วย Secret Key
- ส่ง JWT กลับ: Authentication Server ส่ง JWT กลับไปยัง Client (เบราว์เซอร์หรือแอปพลิเคชัน)
- เก็บ JWT: Client จัดเก็บ JWT ไว้ (เช่น ใน Local Storage, Session Storage, หรือ HttpOnly Cookie)
- เรียกใช้ API: เมื่อ Client ต้องการเรียกใช้ API บน Resource Server จะแนบ JWT ไปกับทุกคำขอ โดยทั่วไปจะอยู่ในส่วนหัว
Authorization: Bearer <JWT> - ยืนยัน JWT: Resource Server ที่ได้รับคำขอพร้อม JWT จะทำการตรวจสอบลายเซ็นของ JWT โดยใช้ Secret Key เดียวกัน (หรือ Public Key หากเป็น RS256) หากลายเซ็นถูกต้องและโทเคนยังไม่หมดอายุ Resource Server จะเชื่อถือข้อมูลใน Payload และอนุญาตให้เข้าถึงทรัพยากรตามสิทธิ์ที่ระบุในโทเคนครับ โดยไม่จำเป็นต้องสอบถาม Authentication Server อีกครั้ง
นี่คือเหตุผลที่เรียกว่า Stateless เพราะ Resource Server ไม่ต้องเก็บสถานะใดๆ ของผู้ใช้งานไว้ มันแค่ตรวจสอบ JWT ที่ได้รับมาก็พอครับ
ข้อดีและข้อจำกัดของ JWT
ข้อดี:
- Statelessness: เซิร์ฟเวอร์ไม่ต้องเก็บสถานะ Session ทำให้ระบบ Scalable ได้ง่ายขึ้น เหมาะสำหรับ Microservices และ API Gateway ที่ไม่ต้องแชร์ Session กัน
- Self-contained: ข้อมูลทั้งหมดที่จำเป็นสำหรับการยืนยันตัวตนและอนุญาตสิทธิ์อยู่ในตัวโทเคนเอง ทำให้ Resource Server ไม่ต้องสอบถามฐานข้อมูลหรือ Authentication Server ทุกครั้ง
- Cross-domain Authentication: สามารถใช้ JWT ข้ามโดเมนได้ง่าย ทำให้เหมาะสำหรับ Single Sign-On (SSO)
- Mobile Friendly: เหมาะสำหรับการใช้งานบนมือถือ เนื่องจากสามารถส่งผ่าน HTTP Header ได้ง่าย
- ความปลอดภัย: ลายเซ็นดิจิทัลช่วยให้มั่นใจได้ว่าข้อมูลในโทเคนไม่ได้ถูกแก้ไข
ข้อจำกัด:
- ขนาดของโทเคน: หากใส่ข้อมูล (Claims) จำนวนมาก โทเคนจะมีขนาดใหญ่ขึ้น ทำให้ HTTP Request Header มีขนาดใหญ่ขึ้นตามไปด้วย
- การเพิกถอนโทเคน (Revocation): เนื่องจาก JWT เป็นแบบ Stateless การเพิกถอนโทเคนก่อนหมดอายุเป็นเรื่องที่ทำได้ยากหรือต้องใช้กลไกเพิ่มเติม (เช่น Blacklist) ซึ่งจะลดทอนความเป็น Stateless ลงไปครับ
- ความเสี่ยงจากการถูกขโมย: หาก Access Token (JWT) ถูกขโมย ผู้โจมตีสามารถใช้งานได้จนกว่าจะหมดอายุ ซึ่งอาจใช้เวลาหลายชั่วโมงหรือหลายวัน
- ไม่ควรเก็บข้อมูลที่ละเอียดอ่อน: แม้จะมีลายเซ็นรับรอง แต่ Payload ไม่ได้ถูกเข้ารหัส (ใน JWS) ดังนั้นจึงไม่ควรใส่ข้อมูลที่ละเอียดอ่อนมากๆ ลงไปใน JWT ที่ไม่ได้เข้ารหัส
การทำงานร่วมกัน: OAuth 2.0 และ JWT Authentication
ตอนนี้เราได้ทำความเข้าใจทั้ง OAuth 2.0 และ JWT แยกกันแล้ว ถึงเวลาที่จะมาดูว่าทั้งสองมาตรฐานนี้ทำงานร่วมกันได้อย่างไร และทำไมจึงเป็นคู่หูที่ทรงพลังสำหรับการจัดการความปลอดภัยในแอปพลิเคชันยุคใหม่ครับ
บทบาทของแต่ละส่วน
- OAuth 2.0: ทำหน้าที่เป็น เฟรมเวิร์กในการมอบสิทธิ์ (Authorization Framework) ที่จัดการขั้นตอนการขอความยินยอมจาก Resource Owner และการออกโทเคน
- JWT: ทำหน้าที่เป็น รูปแบบของโทเคน (Token Format) ที่ใช้ในการบรรจุข้อมูลสิทธิ์ (Authorization) และ/หรือข้อมูลยืนยันตัวตน (Authentication) ที่ถูกออกโดย Authorization Server และนำไปใช้โดย Client เพื่อเข้าถึง Resource Server
พูดง่ายๆ คือ OAuth 2.0 คือ “วิธีการ” ที่ Client ได้รับโทเคน และ JWT คือ “รูปแบบ” ของโทเคนนั้นครับ
ตัวอย่างกระบวนการ Authentication ด้วย OAuth 2.0 และ JWT
เราจะกลับมาที่กระบวนการ Authorization Code Grant + PKCE อีกครั้ง แต่คราวนี้เราจะชี้ให้เห็นว่า JWT เข้ามามีบทบาทตรงไหนบ้างครับ:
- Client เริ่มต้นการขอสิทธิ์: เหมือนเดิม Client redirect Resource Owner ไปยัง Authorization Server พร้อม
code_challengeและพารามิเตอร์อื่นๆ - Resource Owner ให้ความยินยอม: Resource Owner ล็อกอินและให้ความยินยอม
- Authorization Server ส่ง Authorization Code กลับ: Authorization Server redirect Resource Owner กลับไปยัง Client พร้อม
Authorization Code - Client แลกเปลี่ยน Authorization Code เป็น Access Token: Client ส่ง
Authorization Codeและcode_verifier(รวมถึงclient_idและredirect_uri) ไปยัง Authorization Server - Authorization Server ตรวจสอบและออกโทเคน (รวมถึง JWT):
- Authorization Server ตรวจสอบความถูกต้องของคำขอ
- หากถูกต้อง Authorization Server จะ สร้าง JWT ขึ้นมา โดยใส่ข้อมูลผู้ใช้งาน (เช่น
sub,name,role) และสิทธิ์ (scope) ลงใน Payload พร้อมกับเวลาหมดอายุ (exp) และเซ็นด้วย Secret Key - Authorization Server ส่ง
Access Token(ซึ่งก็คือ JWT) และอาจจะมีRefresh TokenและID Token(ในกรณีของ OpenID Connect) กลับให้ Client
- Client ใช้ Access Token (JWT) เพื่อเข้าถึง Resource Server:
- Client เก็บ Access Token (JWT) ไว้
- เมื่อ Client ต้องการเรียกใช้ API บน Resource Server จะแนบ Access Token (JWT) ไปใน HTTP Header:
Authorization: Bearer <JWT>
- Resource Server ตรวจสอบ Access Token (JWT):
- Resource Server รับ JWT มา
- Resource Server ตรวจสอบลายเซ็นของ JWT โดยใช้ Secret Key สาธารณะ (หรือ Public Key) ที่แชร์กับ Authorization Server
- Resource Server ตรวจสอบเวลาหมดอายุ (
expclaim) และตรวจสอบสิทธิ์ (scopeหรือ role claims) ที่ระบุใน Payload ของ JWT - หาก JWT ถูกต้องและมีสิทธิ์ตามที่ร้องขอ Resource Server จะดำเนินการตามคำสั่งและส่งข้อมูลกลับ
จากกระบวนการนี้จะเห็นได้ว่า OAuth 2.0 เป็นกลไกที่ช่วยให้ Client ได้รับ JWT อย่างปลอดภัย ส่วน JWT ก็เป็นรูปแบบที่ใช้ในการบรรจุข้อมูลและยืนยันความถูกต้องของสิทธิ์นั้นๆ ครับ
สถาปัตยกรรมที่แนะนำ
การผสมผสาน OAuth 2.0 และ JWT เหมาะอย่างยิ่งสำหรับสถาปัตยกรรมประเภท:
- Single-Page Applications (SPAs) และ Mobile Apps: โดยใช้ Authorization Code Grant ร่วมกับ PKCE เพื่อให้ Client ได้รับ Access Token (JWT) อย่างปลอดภัย จากนั้น Client ก็จะใช้ JWT ในการเรียก API หลังบ้าน
- Microservices: แต่ละ Microservice สามารถตรวจสอบ JWT ได้ด้วยตัวเอง โดยไม่ต้องไป Query Authentication Service ทุกครั้ง ทำให้ลด Latency และเพิ่ม Scalability ได้
- API Gateways: API Gateway สามารถทำการตรวจสอบ JWT เบื้องต้นได้ก่อนที่จะส่งต่อ Request ไปยัง Microservice ที่เกี่ยวข้อง
การใช้ JWT ในฐานะ Access Token ยังสามารถนำไปต่อยอดใช้กับ OpenID Connect (OIDC) ซึ่งเป็นเลเยอร์ที่สร้างอยู่บน OAuth 2.0 เพื่อเพิ่มความสามารถในการยืนยันตัวตน (Authentication) ให้กับ OAuth 2.0 โดยเฉพาะ โดย OIDC จะใช้ ID Token ซึ่งเป็น JWT อีกประเภทหนึ่ง เพื่อบรรจุข้อมูลยืนยันตัวตนของผู้ใช้งานครับ
การนำไปใช้งานจริง: ตัวอย่าง Code Snippets (Node.js/Express)
เพื่อช่วยให้คุณเห็นภาพการทำงานจริง เราจะมาดูตัวอย่าง Code Snippets ง่ายๆ ในการสร้างและยืนยัน JWT ด้วย Node.js และ Express ครับ
ตั้งค่าโปรเจกต์ Node.js/Express เบื้องต้น
สร้างโปรเจกต์ใหม่และติดตั้ง dependencies ที่จำเป็น:
mkdir jwt-oauth-example
cd jwt-oauth-example
npm init -y
npm install express jsonwebtoken dotenv
สร้างไฟล์ .env สำหรับเก็บ Secret Key:
JWT_SECRET=your_super_secret_key_that_should_be_long_and_random
การสร้าง JWT (บน Authorization Server หรือ API Server)
สมมติว่าคุณมี endpoint สำหรับล็อกอินหรือสร้างโทเคน เราจะใช้ jsonwebtoken ในการสร้าง JWT ครับ
app.js (ส่วนสร้าง JWT):
require('dotenv').config(); // โหลดค่าจาก .env
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const port = 3000;
app.use(express.json()); // สำหรับ Parse JSON body
const JWT_SECRET = process.env.JWT_SECRET;
// Endpoint สำหรับการล็อกอิน (จำลอง) เพื่อสร้าง JWT
app.post('/login', (req, res) => {
const { username, password } = req.body;
// จำลองการตรวจสอบผู้ใช้งาน
if (username === 'user123' && password === 'password123') {
// ข้อมูลที่จะใส่ใน Payload ของ JWT
const userPayload = {
userId: 'a1b2c3d4',
username: username,
roles: ['user'],
scope: ['read:profile', 'write:posts']
};
// สร้าง JWT
// expiresIn: กำหนดเวลาหมดอายุ เช่น '1h' (1 ชั่วโมง), '7d' (7 วัน)
const token = jwt.sign(userPayload, JWT_SECRET, { expiresIn: '1h' });
return res.json({
message: 'Login successful!',
accessToken: token
});
} else {
return res.status(401).json({ message: 'Invalid credentials' });
}
});
app.listen(port, () => {
console.log(`Authorization Server (mock) listening at http://localhost:${port}`);
});
วิธีทดสอบ (ใช้เครื่องมืออย่าง Postman หรือ Insomnia):
POST http://localhost:3000/login
Content-Type: application/json
{
"username": "user123",
"password": "password123"
}
คุณจะได้รับ JSON ตอบกลับพร้อม accessToken (ที่เป็น JWT) ครับ
การยืนยัน JWT (บน Resource Server)
เราจะสร้าง Middleware สำหรับ Express เพื่อตรวจสอบ JWT ในทุกๆ Request ที่ต้องการการยืนยันตัวตน
app.js (ส่วนยืนยัน JWT และ Protected Endpoint):
require('dotenv').config();
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const port = 3000;
app.use(express.json());
const JWT_SECRET = process.env.JWT_SECRET;
// Middleware สำหรับยืนยัน JWT
const authenticateToken = (req, res, next) => {
// ดึงค่า Authorization Header
const authHeader = req.headers['authorization'];
// รูปแบบที่คาดหวัง: "Bearer <TOKEN>"
const token = authHeader && authHeader.split(' ')[1];
if (token == null) {
return res.sendStatus(401); // ถ้าไม่มีโทเคน
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
console.error('JWT Verification Error:', err.message);
return res.sendStatus(403); // ถ้าโทเคนไม่ถูกต้องหรือไม่หมดอายุ
}
req.user = user; // เก็บข้อมูลผู้ใช้งานจาก Payload ไว้ใน req object
next(); // ไปยัง Middleware หรือ Route ถัดไป
});
};
// Endpoint สำหรับการล็อกอิน (ส่วนนี้เหมือนเดิม)
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'user123' && password === 'password123') {
const userPayload = {
userId: 'a1b2c3d4',
username: username,
roles: ['user'],
scope: ['read:profile', 'write:posts']
};
const token = jwt.sign(userPayload, JWT_SECRET, { expiresIn: '1h' });
return res.json({
message: 'Login successful!',
accessToken: token
});
} else {
return res.status(401).json({ message: 'Invalid credentials' });
}
});
// Protected Endpoint ที่ต้องใช้ JWT ในการเข้าถึง
app.get('/profile', authenticateToken, (req, res) => {
// req.user จะมีข้อมูลจาก Payload ของ JWT
res.json({
message: 'Welcome to your profile!',
userData: req.user,
accessTime: new Date().toISOString()
});
});
// อีก Protected Endpoint ที่ต้องการสิทธิ์ 'write:posts'
app.post('/posts', authenticateToken, (req, res) => {
// ตรวจสอบสิทธิ์จาก req.user.scope
if (!req.user.scope || !req.user.scope.includes('write:posts')) {
return res.status(403).json({ message: 'Forbidden: Insufficient scope' });
}
const { title, content } = req.body;
res.status(201).json({
message: 'Post created successfully!',
post: { title, content, author: req.user.username }
});
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
วิธีทดสอบ Protected Endpoint (`/profile`):
- ล็อกอินที่
/loginเพื่อรับaccessToken - นำ
accessTokenที่ได้ไปใช้ใน Header ของ Request สำหรับ/profileGET http://localhost:3000/profile Authorization: Bearer <YOUR_ACCESS_TOKEN_HERE> - คุณควรจะได้รับข้อมูลโปรไฟล์ หากคุณไม่ได้ใส่โทเคน หรือโทเคนไม่ถูกต้อง/หมดอายุ คุณจะได้รับสถานะ 401 หรือ 403 ครับ
วิธีทดสอบ Protected Endpoint (`/posts`):
POST http://localhost:3000/posts
Content-Type: application/json
Authorization: Bearer <YOUR_ACCESS_TOKEN_HERE>
{
"title": "My First Post",
"content": "This is the content of my first post."
}
ถ้าโทเคนถูกต้องและมีสิทธิ์ write:posts คุณจะได้รับข้อความยืนยันการสร้างโพสต์ครับ
ตัวอย่างการเรียกใช้ API ด้วย Access Token (Client-side Conceptual)
นี่คือแนวคิดการเรียกใช้ API จากฝั่ง Client (เช่น JavaScript ในเบราว์เซอร์หรือแอปพลิเคชันมือถือ) หลังจากการล็อกอินและได้รับ Access Token (JWT) มาแล้ว
// สมมติว่าเราเก็บ accessToken ไว้ใน localStorage หลังจากล็อกอิน
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
fetch('http://localhost:3000/profile', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
// โทเคนหมดอายุหรือ invalid, อาจจะต้องล็อกอินใหม่
if (response.status === 401 || response.status === 403) {
console.log('Access token expired or invalid. Please log in again.');
// Redirect ไปหน้า login หรือทำการ refresh token
}
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('User Profile:', data);
// แสดงข้อมูลโปรไฟล์ใน UI
})
.catch(error => {
console.error('Error fetching profile:', error);
});
// ตัวอย่างการเรียกใช้ POST API ที่ต้องการสิทธิ์
fetch('http://localhost:3000/posts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'New Article',
content: 'This is some exciting new content!'
})
})
.then(response => response.json())
.then(data => console.log('Post creation response:', data))
.catch(error => console.error('Error creating post:', error));
} else {
console.log('No access token found. Please log in.');
// Redirect ไปหน้า login
}
ตัวอย่างเหล่านี้แสดงให้เห็นถึงกระบวนการพื้นฐานในการสร้างและยืนยัน JWT ใน Node.js/Express ซึ่งเป็นแนวทางที่ใช้กันอย่างแพร่หลายในแอปพลิเคชันสมัยใหม่ครับ
ข้อควรพิจารณาด้านความปลอดภัยและการจัดการ
แม้ว่า OAuth 2.0 และ JWT จะเป็นเครื่องมือที่ทรงพลัง แต่การนำไปใช้งานจริงก็ต้องคำนึงถึงข้อควรพิจารณาด้านความปลอดภัยและการจัดการ เพื่อป้องกันช่องโหว่ที่อาจเกิดขึ้นได้ครับ
การจัดการ Refresh Token
Access Token (JWT) มักจะมีอายุสั้น (เช่น 15 นาทีถึง 1 ชั่วโมง) เพื่อจำกัดความเสียหายหากถูกขโมย แต่การที่ผู้ใช้งานต้องล็อกอินใหม่บ่อยๆ ก็ไม่ใช่ประสบการณ์ที่ดี ดังนั้นจึงมี Refresh Token เข้ามาช่วย
- Refresh Token: เป็นโทเคนที่มีอายุยืนยาวกว่า (เช่น 7 วัน, 30 วัน หรือถาวร) ใช้สำหรับขอ Access Token ใหม่ เมื่อ Access Token เดิมหมดอายุ โดยไม่ต้องให้ผู้ใช้งานล็อกอินใหม่
- การจัดเก็บ: Refresh Token ควรถูกจัดเก็บอย่างปลอดภัยที่สุด มักจะเก็บไว้ใน HttpOnly Cookie (สำหรับ Web Apps) หรือ Secure Storage (สำหรับ Mobile Apps)
- การเพิกถอน: Refresh Token ควรสามารถเพิกถอนได้ (Revocation) ทันทีที่ผู้ใช้งานล็อกเอาต์ หรือเมื่อตรวจพบความผิดปกติ เพื่อป้องกันการใช้งานโดยไม่ได้รับอนุญาต
การจัดเก็บ Access Token
การจัดเก็บ Access Token (JWT) ที่ฝั่ง Client เป็นสิ่งสำคัญที่ต้องพิจารณาอย่างรอบคอบ
- Local Storage/Session Storage: สะดวกในการเข้าถึงด้วย JavaScript แต่เสี่ยงต่อการโจมตีแบบ Cross-Site Scripting (XSS) หากมีช่องโหว่ XSS ผู้โจมตีสามารถขโมยโทเคนไปใช้ได้
- HttpOnly Cookie: เป็นทางเลือกที่ปลอดภัยกว่าสำหรับเว็บแอปพลิเคชัน เพราะ JavaScript ไม่สามารถเข้าถึง Cookie ประเภทนี้ได้โดยตรง ทำให้ป้องกัน XSS ได้ดีกว่า แต่ก็มีข้อจำกัดเรื่อง Cross-Site Request Forgery (CSRF) ที่ต้องป้องกันด้วย SameSite Cookie (Strict/Lax) และ CSRF Token
- Memory: การเก็บไว้ในหน่วยความจำของแอปพลิเคชัน (สำหรับ Mobile/Desktop Apps) เป็นวิธีที่ปลอดภัยที่สุด แต่โทเคนจะหายไปเมื่อแอปพลิเคชันถูกปิด
สำหรับ Single-Page Applications (SPAs) ที่ไม่สามารถใช้ HttpOnly Cookie ได้อย่างมีประสิทธิภาพ (เนื่องจากต้องการส่ง JWT ใน HTTP Header ด้วย JavaScript) การใช้ Local Storage ร่วมกับการป้องกัน XSS อย่างเข้มงวดเป็นสิ่งที่จำเป็นครับ หรืออีกทางเลือกคือการใช้ Backend For Frontend (BFF) เพื่อให้ Backend จัดการ Cookie และส่งต่อ Request ไปยัง API ครับ
การเพิกถอนโทเคน (Token Revocation)
ดังที่กล่าวไป JWT เป็น Stateless การเพิกถอน Access Token ก่อนหมดอายุจึงเป็นเรื่องที่ท้าทาย หาก Access Token ถูกขโมย ผู้โจมตีจะสามารถใช้งานได้จนกว่าจะหมดอายุ
แนวทางแก้ไข:
- อายุโทเคนสั้น: กำหนดให้ Access Token มีอายุสั้นที่สุดเท่าที่จะเป็นไปได้ เพื่อจำกัดช่วงเวลาที่ผู้โจมตีสามารถใช้โทเคนได้
- Blacklist/Denylist: สร้างรายการโทเคนที่ถูกเพิกถอนไว้บนเซิร์ฟเวอร์ เมื่อ Resource Server ได้รับ JWT จะตรวจสอบกับ Blacklist ก่อน หากอยู่ในรายการก็จะปฏิเสธ (วิธีนี้ทำให้ระบบไม่เป็น Stateless อย่างสมบูรณ์)
- Online Validation: Resource Server อาจจะต้องสอบถาม Authorization Server เพื่อยืนยันความถูกต้องของโทเคนทุกครั้ง (ซึ่งก็ลดทอนประโยชน์ของ JWT ลงไป)
- เน้นการเพิกถอน Refresh Token: เมื่อผู้ใช้งานล็อกเอาต์ ควรเพิกถอน Refresh Token ทันที เพื่อป้องกันไม่ให้ผู้โจมตีสร้าง Access Token ใหม่ได้อีก
การป้องกัน Replay Attacks
Replay Attack คือการที่ผู้โจมตีดักจับคำขอ HTTP ที่มี JWT และนำคำขอนั้นไปส่งซ้ำเพื่อเข้าถึงทรัพยากร
- ใช้ HTTPS เสมอ: การเข้ารหัสการสื่อสารทั้งหมดด้วย HTTPS เป็นสิ่งสำคัญที่สุดในการป้องกันการดักจับโทเคน
- อายุโทเคนสั้น: ช่วยจำกัดช่วงเวลาที่โทเคนสามารถถูก Replay ได้
- Nonce (Number Once): สำหรับบาง Flow ของ OAuth (โดยเฉพาะ OpenID Connect) สามารถใช้ Nonce เพื่อป้องกัน Replay Attack ในกระบวนการ Authorization ได้
การกำหนด Scope ที่เหมาะสม
ในการขอสิทธิ์ด้วย OAuth 2.0 การกำหนด scope ที่เหมาะสมมีความสำคัญอย่างยิ่ง
- หลักการ Least Privilege: Client ควรได้รับสิทธิ์ในการเข้าถึงข้อมูลเท่าที่จำเป็นเท่านั้น ไม่ควรขอสิทธิ์มากเกินไป
- ความชัดเจน:
scopeควรมีความหมายที่ชัดเจนและเข้าใจง่ายสำหรับผู้ใช้งาน เพื่อให้ผู้ใช้งานตัดสินใจได้ว่าจะให้ความยินยอมหรือไม่
การเข้าใจและจัดการกับประเด็นเหล่านี้จะช่วยให้ระบบของคุณมีความปลอดภัยและแข็งแกร่งมากยิ่งขึ้นครับ การลงทุนในความปลอดภัยตั้งแต่เริ่มต้นจะช่วยประหยัดเวลาและทรัพยากรในระยะยาวได้อย่างมหาศาลครับ อ่านเพิ่มเติม เกี่ยวกับแนวทางปฏิบัติที่ดีที่สุดในการรักษาความปลอดภัย API ได้ที่นี่ครับ
ตารางเปรียบเทียบ: Session-based vs. Token-based Authentication
เพื่อช่วยให้คุณตัดสินใจได้ว่าระบบ Authentication แบบใดที่เหมาะสมกับโปรเจกต์ของคุณ เรามาดูตารางเปรียบเทียบข้อดีและข้อจำกัดระหว่าง Session-based และ Token-based (โดยใช้ JWT) Authentication กันครับ
| คุณสมบัติ | Session-based Authentication | Token-based Authentication (JWT) |
|---|---|---|
| สถานะ (State) | Stateful: เซิร์ฟเวอร์ต้องเก็บสถานะ Session (เช่น Session ID ในฐานข้อมูล) | Stateless: เซิร์ฟเวอร์ไม่ต้องเก็บสถานะ Session โทเคนมีข้อมูลครบถ้วน |
| Scalability | จัดการยากในระบบกระจายตัว (Distributed Systems) และ Microservices เพราะต้องแชร์ Session หรือใช้ Sticky Session | Scalable ง่าย เพราะแต่ละ Request มีข้อมูลในตัว ไม่ต้องพึ่งพาเซิร์ฟเวอร์อื่น |
| Cross-domain / SSO | ยากกว่า ต้องใช้เทคนิคพิเศษ เช่น Shared Session Storage หรือ Cookie Domain | ง่ายกว่า สามารถใช้โทเคนข้ามโดเมนได้ดี เหมาะสำหรับ SSO |
| การจัดเก็บข้อมูลผู้ใช้งาน | Session ID ใน Cookie (เซิร์ฟเวอร์เก็บข้อมูลผู้ใช้) | Access Token (JWT) ใน Local Storage, Session Storage, หรือ Cookie (ข้อมูลผู้ใช้ใน Payload) |
| ความเสี่ยงด้านความปลอดภัย |
|
|
| การเพิกถอนโทเคน | ง่าย: ลบ Session ID ออกจากเซิร์ฟเวอร์ได้ทันที | ยาก: ต้องมีกลไก Blacklist หรือกำหนดอายุโทเคนสั้นๆ |
| ประสิทธิภาพ | อาจมี Overhead ในการ Query ฐานข้อมูล Session | เร็ว: การตรวจสอบโทเคนทำได้ด้วยตัวเอง (Self-contained) ไม่ต้อง Query ฐานข้อมูล |
| การใช้งาน (Client) | ส่วนใหญ่ใช้กับ Web Application โดยพึ่งพา Cookie | ยืดหยุ่นกว่า ใช้ได้ทั้ง Web (SPA), Mobile, Desktop, Server-to-Server |
โดยรวมแล้ว Token-based Authentication ด้วย JWT มักเป็นตัวเลือกที่ได้รับความนิยมในสถาปัตยกรรมแอปพลิเคชันสมัยใหม่ โดยเฉพาะอย่างยิ่งสำหรับ API-driven applications และ Microservices เนื่องจากความยืดหยุ่นและความสามารถในการ Scalability ครับ แต่ก็ต้องมีการจัดการความปลอดภัยอย่างรอบคอบเพื่อลดความเสี่ยงที่มาพร้อมกับความเป็น Stateless ของมันครับ
คำถามที่พบบ่อย (FAQ)
Q1: OAuth 2.0 คือ Authentication หรือ Authorization?
A1: OAuth 2.0 เป็น Authorization (การมอบสิทธิ์) ครับ ไม่ใช่ Authentication (การยืนยันตัวตน) โดยตรง มันช่วยให้แอปพลิเคชันสามารถเข้าถึงทรัพยากรของผู้ใช้งานในบริการอื่นได้ โดยไม่ต้องรู้รหัสผ่านของผู้ใช้งาน แต่ไม่ได้ยืนยันตัวตนของผู้ใช้งานนั้นๆ หากต้องการ Authentication ด้วย ต้องใช้ร่วมกับ OpenID Connect (OIDC) ซึ่งเป็นเลเยอร์ที่อยู่บน OAuth 2.0 อีกทีครับ
Q2: ทำไมไม่ควรเก็บ JWT Access Token ใน Local Storage?
A2: การเก็บ JWT Access Token ใน Local Storage มีความเสี่ยงต่อการโจมตีแบบ Cross-Site Scripting (XSS) ครับ หากเว็บไซต์ของคุณมีช่องโหว่ XSS ผู้โจมตีสามารถ inject malicious JavaScript code เข้าไปในหน้าเว็บ และโค้ดนั้นจะสามารถเข้าถึง Local Storage เพื่อขโมย JWT Access Token ของผู้ใช้งานไปใช้ได้ครับ แม้ว่าจะสะดวกในการเข้าถึงด้วย JavaScript แต่ก็เป็นความเสี่ยงที่สำคัญครับ
Q3: ควรตั้งค่าอายุของ Access Token (JWT) และ Refresh Token นานแค่ไหน?
A3: โดยทั่วไปแล้ว Access Token ควรกำหนดให้มีอายุค่อนข้างสั้น (เช่น 15 นาทีถึง 1 ชั่วโมง) เพื่อลดความเสียหายหากโทเคนถูกขโมยครับ ส่วน Refresh Token สามารถมีอายุที่ยาวนานกว่า (เช่น 7 วัน, 30 วัน, หรือจนกว่าจะมีการเพิกถอน) เพราะ Refresh Token ควรถูกจัดเก็บอย่างปลอดภัยกว่าและใช้เพื่อขอ Access Token ใหม่เท่านั้นครับ การปรับอายุโทเคนขึ้นอยู่กับความต้องการด้านความปลอดภัยและประสบการณ์ผู้ใช้งานของแต่ละแอปพลิเคชันครับ
Q4: ถ้า JWT เป็นแบบ Stateless แล้วจะทำ Token Revocation ได้อย่างไร?
A4: การเพิกถอน JWT Access Token ก่อนหมดอายุเป็นความท้าทายของระบบ Stateless ครับ วิธีที่นิยมใช้คือการสร้าง Blacklist (หรือ Denylist) บนเซิร์ฟเวอร์ โดยเก็บรายการ JWT ID (jti claim) ของโทเคนที่ไม่ต้องการให้ใช้งานอีกต่อไป เมื่อ Resource Server ได้รับ JWT ก็จะตรวจสอบกับ Blacklist นี้ก่อน หาก jti อยู่ใน Blacklist ก็จะปฏิเสธการเข้าถึงครับ อีกวิธีหนึ่งคือการกำหนดอายุโทเคนให้สั้นมากๆ เพื่อลดระยะเวลาที่โทเคนสามารถถูกนำไปใช้ได้หากถูกขโมยไปครับ
Q5: PKCE คืออะไร และทำไมจึงสำคัญสำหรับ Public Clients?
A5: PKCE (Proof Key for Code Exchange) คือส่วนเสริมด้านความปลอดภัยสำหรับ Authorization Code Grant ใน OAuth 2.0 ครับ มันช่วยป้องกันการโจมตีที่เรียกว่า “Authorization Code Interception Attack” โดยเฉพาะสำหรับ Public Clients (เช่น Single-Page Applications หรือ Mobile Apps) ที่ไม่สามารถเก็บ client_secret ได้อย่างปลอดภัย PKCE ทำให้มั่นใจได้ว่า Client ที่ได้รับ Authorization Code คือ Client เดียวกันกับที่เริ่มกระบวนการขอ Authorization Code ในตอนแรก ทำให้ Authorization Code Grant ปลอดภัยยิ่งขึ้นสำหรับแอปพลิเคชันประเภทนี้ครับ
Q6: OpenID Connect (OIDC) เกี่ยวข้องกับ OAuth 2.0 และ JWT อย่างไร?
A6: OpenID Connect (OIDC) เป็นเลเยอร์ของการยืนยันตัวตน (Authentication) ที่สร้างอยู่บน OAuth 2.0 ครับ โดย OIDC ใช้ OAuth 2.0 เป็นเฟรมเวิร์กในการมอบสิทธิ์ และใช้ JWT เป็นรูปแบบของ ID Token เพื่อส่งข้อมูลยืนยันตัวตนของผู้ใช้งานจาก Authorization Server ไปยัง Client ID Token จะเป็น JWT ที่มี Claims ที่ระบุข้อมูลผู้ใช้งาน เช่น sub (Subject ID), name, email เป็นต้น ทำให้ Client สามารถยืนยันตัวตนของผู้ใช้งานได้อย่างปลอดภัยครับ
สรุปและก้าวต่อไป
ในบทความนี้ เราได้สำรวจแนวคิดเบื้องหลังและรายละเอียดเชิงลึกของ OAuth 2.0 และ JWT Authentication อย่างครบถ้วนแล้วนะครับ เราได้เรียนรู้ว่า OAuth 2.0 เป็นมาตรฐานสำหรับการมอบสิทธิ์ที่ช่วยให้แอปพลิเคชันสามารถเข้าถึงทรัพยากรของผู้ใช้งานได้อย่างปลอดภัย โดยไม่ต้องเปิดเผยรหัสผ่าน และ JWT เป็นรูปแบบโทเคนแบบ Self-contained ที่ใช้ในการส่งข้อมูลที่เชื่อถือได้ พร้อมลายเซ็นดิจิทัลที่รับรองความสมบูรณ์ของข้อมูลครับ
เมื่อนำทั้งสองมารวมกัน OAuth 2.0 จะทำหน้าที่เป็นกลไกในการขอและออก Access Token (ซึ่งมักจะเป็น JWT) และ JWT ก็จะทำหน้าที่เป็น “กุญแจดิจิทัล” ที่ Client ใช้เพื่อเข้าถึง API หรือทรัพยากรต่างๆ โดย Resource Server สามารถตรวจสอบความถูกต้องของกุญแจนี้ได้ด้วยตัวเองโดยไม่ต้องพึ่งพา Authorization Server ทุกครั้ง ทำให้ระบบมีความยืดหยุ่น, Scalable และมีประสิทธิภาพสูงขึ้นครับ
การนำ OAuth 2.0 และ JWT ไปใช้งานจริงนั้นต้องอาศัยความเข้าใจที่ลึกซึ้งในเรื่องความปลอดภัย โดยเฉพาะอย่างยิ่งการจัดการ Refresh Token, การจัดเก็บ Access Token อย่างปลอดภัย, และการพิจารณาถึงกลไกการเพิกถอนโทเคนครับ การเลือก Grant Type ที่เหมาะสม และการนำ PKCE มาใช้ จะช่วยเสริมความแข็งแกร่งให้กับระบบของคุณได้อย่างมากเลยทีเดียว
หวังว่าบทความนี้จะเป็นคู่มือฉบับสมบูรณ์ที่จะช่วยให้คุณมีความเข้าใจใน OAuth 2.0 และ JWT Authentication มากยิ่งขึ้น และสามารถนำความรู้เหล่านี้ไปประยุกต์ใช้ในการสร้างแอปพลิเคชันที่ปลอดภัยและมีประสิทธิภาพได้นะครับ หากคุณมีข้อสงสัยเพิ่มเติม หรือต้องการศึกษาในหัวข้อใดเป็นพิเศษ อย่าลังเล