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

Authentication & Context

คำถามแรกในสี่ข้อคือ ใครเป็นคนถาม? คำตอบของคำถามนี้คือ authentication — การเปลี่ยน credential ที่อ่านความหมายไม่ได้ (โดยทั่วไปคือ token ใน HTTP header) ให้กลายเป็น identity ที่รู้จัก จุดสำคัญของ GraphQL คือ ตำแหน่ง ที่งานนี้เกิดขึ้น ให้ทำ ครั้งเดียวต่อ request ก่อน resolver ตัวไหนจะรัน แล้วส่งผลลัพธ์ไปให้ทุก resolver ผ่าน object ที่ใช้ร่วมกันชื่อ context

เมื่อ 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
token ถูก verify หนึ่งครั้งที่ขอบ; resolver เพียงอ่าน identity ที่ได้ออกมาเท่านั้น

ความผิดพลาดของมือใหม่ที่เจอบ่อยคือเอา 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 ใหญ่ ๆ

ตัวอย่างด้านล่างสร้าง schema เล็ก ๆ ที่มีสอง field me คืนชื่อของ user ที่ authenticate แล้ว หรือ null เมื่อไม่มีใครล็อกอิน ส่วน serverTime เป็น field สาธารณะที่ไม่สนใจ identity เลย ที่สำคัญคือ resolver ไม่เคยแตะ token เลย แค่อ่าน context.user ที่ context factory resolve ไว้ให้ล่วงหน้า กด Run เพื่อรันด้วย GraphQL engine จริง โดย context ถูกส่งเข้าไปในชื่อ contextValue

JavaScript

ดู 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 ใน testcontext ที่ไม่มี 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 ใน contextValue factory

💡 ตัวอย่างจากของจริง

Apollo Server:

  • contextValue factory รัน per-request — inject user จาก JWT ทุกครั้ง
  • context.user พร้อมใช้ใน resolver ทุกตัวโดยไม่ต้อง parse token ซ้ำ

GraphQL Yoga:

  • context factory รองรับ async — ทำ database lookup สำหรับ user ได้ใน context
GraphQL server ควร verify token ที่เข้ามาและ resolve current user ที่ใด?
context factory ควรคืนอะไรสำหรับ anonymous request ที่ไม่มี token ที่ถูกต้อง?
ในตัวอย่างที่รันได้ ทำไม resolver me จึงไม่จำเป็นต้องอ่าน Authorization header?