Authorization
ในบทเรียนที่แล้วเราจัดการเรื่อง ใครอยู่บนการเชื่อมต่อ บทเรียนนี้เป็นเรื่องคำถามที่ใหญ่กว่ามากซึ่งตามมา: message ตัวนี้โดยเฉพาะได้รับอนุญาตไหม? Authentication เกิดขึ้น ครั้งเดียว; authorization เกิดขึ้น ทุก message การรวมสองสิ่งนี้เข้าด้วยกันเป็นหนึ่งใน WebSocket bug ที่พบบ่อยที่สุดและแพงที่สุด
Authenticate ครั้งเดียว authorize ทุก action
หัวข้อที่มีชื่อว่า “Authenticate ครั้งเดียว authorize ทุก action”HTTP API ตรวจ permission ใหม่ในแต่ละ request แทบจะโดยบังเอิญ — แต่ละ request แยกกัน ดังนั้นแต่ละอันจึงวิ่งผ่าน middleware ของคุณ การเชื่อมต่อ WebSocket ทำลายนิสัยนั้น หลังจาก handshake message เป็นร้อยมาถึงบนการเชื่อมต่อที่ authenticate แล้ว อันเดียว และชวนให้คิดว่า “การเชื่อมต่อนี้ล็อกอินอยู่ ดังนั้น message ของตัวเองก็โอเค”
ไม่โอเค ล็อกอินอยู่ ไม่ใช่ ได้รับอนุญาต ผู้ใช้ที่ authenticate เป็น alice อาจส่ง:
{ "type": "delete", "room": "admins", "messageId": "42" }alice จะลบ message นั้นได้ไหมไม่เกี่ยวกับว่า alice authenticate แล้วหรือไม่ — แต่ขึ้นอยู่กับ role ของ alice, การเป็นสมาชิกในห้องนั้น, และความสัมพันธ์ของเธอกับ message นั้นโดยเฉพาะ การตัดสินใจนั้นต้องทำใหม่ ฝั่ง server ตอนที่มาถึง
อย่าไว้ใจตัวตนที่ client อ้าง
หัวข้อที่มีชื่อว่า “อย่าไว้ใจตัวตนที่ client อ้าง”นี่คือบาปร้ายแรง และมีอยู่ทุกที่ message ข้างบนอาจมาถึงในรูปแบบนี้แทน:
{ "type": "delete", "user": "admin", "room": "admins", "messageId": "42" }server ที่ไร้เดียงสาอ่าน msg.user แล้วคิดว่า “อา admin ขอมา” แต่ client เขียน field นั้นเอง client อ้างเป็นใครก็ได้ ตัวตนที่เชื่อถือได้เพียงอย่างเดียวคือตัวตนที่ server ของคุณ pin ไว้กับการเชื่อมต่อตอน handshake — ws.user จากบทเรียนที่แล้ว — ซึ่งสร้างจาก credential ที่ validate แล้วและ client ปลอมแปลงไม่ได้
ดังนั้นกฎจึงตรงไปตรงมา: เพิกเฉยต่อตัวตนใด ๆ ใน body ของ message Authorize เทียบกับ ws.user ค่าที่คุณตั้ง ไม่ใช่เทียบกับ field user/role/isAdmin ที่ client ส่งมา ถ้า client เขียนค่านั้นเองได้ ค่านั้นก็เป็นแค่ข้ออ้าง ไม่ใช่ข้อเท็จจริง
flowchart TB
M["Incoming message<br/>(may CLAIM a user/role)"] --> I["Ignore claimed identity"]
I --> ID["Use ws.user — pinned at handshake"]
ID --> P{"Is ws.user allowed<br/>to do THIS action<br/>on THIS resource?"}
P -- "yes" --> A["Perform action"]
P -- "no" --> D["Reject: error frame<br/>(or close on abuse)"] เดโม authorization ราย message ที่รันได้จริง
หัวข้อที่มีชื่อว่า “เดโม authorization ราย message ที่รันได้จริง”เดโมด้านล่างคือ body ของ ws connection handler handshake ได้ pin ตัวตนไว้กับ socket แล้ว — ตรงนี้เรา hard-code ws.user และตาราง role เพื่อให้เดโมอยู่ครบในตัว จากนั้น ทุก message ที่เข้ามาจะถูก authorize: เรา look up role จริง ของ ws.user และเราจงใจ เพิกเฉย ต่อ field user ที่ client ส่งมา delete อนุญาตเฉพาะ admin เท่านั้น; post อนุญาตทุกคน เปิดเดโมใน StackBlitz แล้วให้ client ส่ง message ชนิดต่าง ๆ
Needs the Node.js runtime — open in StackBlitz to run.
เพราะ ws.user คือ alice ที่เป็น member, post จึงได้รับอนุญาตแต่ delete ถูกปฏิเสธ — แม้ว่า client จะเพิ่ม "user": "admin" ลงใน message เพราะ server ไม่เคยอ่าน field นั้น สลับ ws.user เป็น 'bob' แล้ว delete จะได้รับอนุญาต นั่นคือวินัยทั้งหมด: ตัวตนมาจากการเชื่อมต่อ, permission ถูกคำนวณใหม่ราย message, และ body ของ message ถูกมองเป็นคำขอ ไม่ใช่แหล่งของความจริง
| Authorization Check | เมื่อไร | ตัวอย่าง |
|---|---|---|
| Connection level | หลัง handshake | user ต้อง authenticate ก่อนส่ง message ใด ๆ |
| Room/Channel level | ก่อน join | user มีสิทธิ์ join channel นี้ไหม |
| Message level | ก่อน process | user มีสิทธิ์ส่ง message type นี้ไหม |
| Resource level | ก่อน action | user เป็นเจ้าของ document นี้ไหม |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Check Auth ที่ Connection เท่านั้น อาการ:
- validate token เมื่อ connect แต่ไม่ check permission ต่อ room หรือ action
- user authenticate แล้วสามารถ join ทุก room และส่ง message ทุกประเภท
- check permission ทุก action: join room, send message, subscribe channel
Token ที่ Expire แต่ Connection ยังอยู่ อาการ:
- user connect ด้วย JWT ที่ expire ใน 1 ชั่วโมง
- หลัง 1 ชั่วโมง connection ยังเปิดอยู่ — user ยังส่ง message ได้
- validate token expiry ต่อ message หรือ implement token refresh ผ่าน WebSocket
💡 ตัวอย่างจากของจริง
Slack:
- authorization ต่อ channel — ไม่ใช่ทุก user เห็นทุก channel
- message ที่ส่งไปยัง channel ที่ user ไม่มีสิทธิ์ → reject
GitHub Codespaces:
- authorization ต่อ codespace — เข้าถึงได้เฉพาะ owner และ collaborator
- terminal WebSocket reject ถ้า token ไม่มีสิทธิ์ต่อ codespace นั้น