Authentication & Context
คำถามแรกในสี่ข้อคือ ใครเป็นคนถาม? คำตอบของคำถามนี้คือ authentication — การเปลี่ยน credential ที่อ่านความหมายไม่ได้ (โดยทั่วไปคือ token ใน HTTP header) ให้กลายเป็น identity ที่รู้จัก จุดสำคัญของ GraphQL คือ ตำแหน่ง ที่งานนี้เกิดขึ้น ให้ทำ ครั้งเดียวต่อ request ก่อน resolver ตัวไหนจะรัน แล้วส่งผลลัพธ์ไปให้ทุก resolver ผ่าน object ที่ใช้ร่วมกันชื่อ context
Context object
หัวข้อที่มีชื่อว่า “Context object”เมื่อ GraphQL server รัน request จะร้อย context value ค่าเดียวผ่านทุก resolver คุณเห็น signature นี้มาแล้ว resolver รับ (parent, args, context, info) ส่วน context เป็นของคุณที่จะเติมเอง เป็นที่อยู่ตามธรรมชาติของอะไรก็ตามที่จริงตลอด ทั้ง request แต่ไม่ใช่ส่วนหนึ่งของ schema เช่น current user, database connection, request-scoped cache และ logger
Authentication คือคนเติม context โดยมี function เล็ก ๆ ที่มักเรียกว่า context factory รันครั้งเดียวตอน request มาถึง หน้าที่คืออ่าน Authorization header, verify token, look up user แล้วคืน context object กลับมา จากนั้นทุก resolver ก็แค่อ่าน context.user
sequenceDiagram
participant C as Client
participant F as Context factory
participant V as Token verifier
participant R as Resolvers
C->>F: Request + Authorization header
F->>V: Verify token (once)
V-->>F: user (or null if anonymous)
F-->>R: context = { user }
R->>R: read context.user
R-->>C: Response Authenticate ที่ขอบ ไม่ใช่ใน schema
หัวข้อที่มีชื่อว่า “Authenticate ที่ขอบ ไม่ใช่ใน schema”ความผิดพลาดของมือใหม่ที่เจอบ่อยคือเอา authentication ไปวางไว้ ข้างใน field เช่นเพิ่ม field login(token: String) หรือให้ resolver แต่ละตัว parse Authorization header ซ้ำ ๆ อย่าทำแบบนั้น schema มีไว้อธิบายข้อมูล ไม่ใช่อธิบายวิธีถอดรหัส credential ถอด token ครั้งเดียวที่ขอบระบบ แล้วปล่อยให้ schema สะอาดไว้:
// Runs once per request, before any resolver. With GraphQL Yoga or Apollo// Server you supply this as the `context` factory.async function createContext({ request }: { request: Request }) { const header = request.headers.get('authorization') ?? ''; const token = header.replace(/^Bearer\s+/i, '');
// verifyToken throws or returns null for bad/expired tokens. const user = token ? await verifyToken(token) : null;
// Note: we do NOT throw here for anonymous requests. Some fields are // public. Authentication establishes identity; it does not grant access. return { user };}มีจุดออกแบบสองข้อที่ควรขีดเส้นใต้ ข้อแรก factory คืน user เป็น null สำหรับ anonymous request แทนที่จะปฏิเสธ request ไปเลย เพราะ field จำนวนมาก (แค็ตตาล็อกสินค้าสาธารณะ, query ของหน้า marketing) ตั้งใจให้เข้าถึงได้โดยไม่ต้องล็อกอิน authentication ตอบว่า “ใคร” ไม่ใช่ “ทำได้ไหม” ส่วนคำถาม “ทำได้ไหม” คือ authorization ที่เป็นหัวข้อของบทถัดไป ข้อสอง token ถูก verify แค่ครั้งเดียว งานที่แพงอย่างการ verify signature และการ look up user จึงไม่ทำซ้ำทุก field ใน query ใหญ่ ๆ
resolver ที่อ่าน context
หัวข้อที่มีชื่อว่า “resolver ที่อ่าน context”ตัวอย่างด้านล่างสร้าง schema เล็ก ๆ ที่มีสอง field me คืนชื่อของ user ที่ authenticate แล้ว หรือ null เมื่อไม่มีใครล็อกอิน ส่วน serverTime เป็น field สาธารณะที่ไม่สนใจ identity เลย ที่สำคัญคือ resolver ไม่เคยแตะ token เลย แค่อ่าน context.user ที่ context factory resolve ไว้ให้ล่วงหน้า กด Run เพื่อรันด้วย GraphQL engine จริง โดย context ถูกส่งเข้าไปในชื่อ contextValue
ดู output ทั้งสองให้ดี ตอนมี user ที่ verify แล้วบน context me คืน "Ada Lovelace" พอ user: null me ก็กลับมาเป็น null ส่วน serverTime ยังทำงานปกติ เพราะ field สาธารณะไม่สนว่าใครเป็นคนถาม resolver เหมือนเดิมเป๊ะทั้งสองรอบ มีแค่ context เท่านั้นที่เปลี่ยน นั่นแหละคือประเด็น — identity ถูกสร้างครั้งเดียวนอก resolver แล้ว schema ก็อ่านเหมือนข้อเท็จจริงระดับ request อื่น ๆ
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| context ทำให้แชร์ auth information ข้าม resolver ได้ | context ที่ใหญ่เกินทำให้ dependency ไม่ชัดเจน |
| inject user object เพียงครั้งเดียว — resolver ใช้ได้ทันที | context object ที่ mutable อาจเกิด race condition |
| แยก auth logic ออกจาก resolver — resolver บาง | context factory ที่ fail ทำให้ทุก request fail |
| ง่ายต่อการ test — inject mock context ใน test | context ที่ไม่มี type safety ทำให้ resolver เข้าถึง field ผิด |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Auth Logic ใน Resolver แต่ละตัว อาการ:
- ทุก resolver check
if (!token) throw new AuthenticationError() - duplicate code และ inconsistent enforcement
- ทำ auth check ใน context factory หรือ middleware — resolver ไม่ต้องรู้
Context ที่ Leak ข้าม Request อาการ:
- context object เป็น singleton ที่ share ข้าม request
- user A อาจเห็นข้อมูลของ user B
- สร้าง context object ใหม่ทุก request ใน
contextValuefactory
💡 ตัวอย่างจากของจริง
Apollo Server:
contextValuefactory รัน per-request — inject user จาก JWT ทุกครั้งcontext.userพร้อมใช้ใน resolver ทุกตัวโดยไม่ต้อง parse token ซ้ำGraphQL Yoga:
contextfactory รองรับ async — ทำ database lookup สำหรับ user ได้ใน context