ข้ามไปยังเนื้อหา

Authorization

ในบทเรียนที่แล้วเราจัดการเรื่อง ใครอยู่บนการเชื่อมต่อ บทเรียนนี้เป็นเรื่องคำถามที่ใหญ่กว่ามากซึ่งตามมา: message ตัวนี้โดยเฉพาะได้รับอนุญาตไหม? Authentication เกิดขึ้น ครั้งเดียว; authorization เกิดขึ้น ทุก message การรวมสองสิ่งนี้เข้าด้วยกันเป็นหนึ่งใน WebSocket bug ที่พบบ่อยที่สุดและแพงที่สุด

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 ตอนที่มาถึง

นี่คือบาปร้ายแรง และมีอยู่ทุกที่ message ข้างบนอาจมาถึงในรูปแบบนี้แทน:

{ "type": "delete", "user": "admin", "room": "admins", "messageId": "42" }

server ที่ไร้เดียงสาอ่าน msg.user แล้วคิดว่า “อา admin ขอมา” แต่ client เขียน field นั้นเอง client อ้างเป็นใครก็ได้ ตัวตนที่เชื่อถือได้เพียงอย่างเดียวคือตัวตนที่ server ของคุณ pin ไว้กับการเชื่อมต่อตอน handshakews.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 เทียบกับตัวตนที่ pin ไว้ของการเชื่อมต่อ

เดโมด้านล่างคือ 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 ชนิดต่าง ๆ

Node.js

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หลัง handshakeuser ต้อง authenticate ก่อนส่ง message ใด ๆ
Room/Channel levelก่อน joinuser มีสิทธิ์ join channel นี้ไหม
Message levelก่อน processuser มีสิทธิ์ส่ง message type นี้ไหม
Resource levelก่อน actionuser เป็นเจ้าของ 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 นั้น
authorization ต้องรันบ่อยแค่ไหนบนการเชื่อมต่อ WebSocket?
client ส่ง { "type": "delete", "user": "admin" } มา server ควร authorize เทียบกับอะไร?
ทำไมแค่ authenticate การเชื่อมต่อถึงยังไม่พอที่จะอนุญาต action?