Security & AI Architecture

OAuth vs OAuth2: ความแตกต่างทางสถาปัตยกรรมสำหรับ AI และ MCP

คำตอบด่วน: OAuth 1.0a ใช้ลายมือชื่อที่ซับซ้อน ส่วน OAuth 2.0 มีจุดอ่อนที่ Bearer Token แต่สำหรับ AI Agent และเซิร์ฟเวอร์ MCP นั้น OAuth 2.1 เป็นมาตรฐานบังคับ โดยกำหนดให้ใช้ PKCE (RFC 7636) ตัดโฟลว์ที่ไม่ปลอดภัย และผสาน DPoP (RFC 9449) เพื่อผูกโทเค็นแบบ Zero-Trust บนสภาพแวดล้อม Headless


1. บทนำ: วิกฤตอัตลักษณ์และการอนุญาตสิทธิ์ของ AI Agent ในปี 2026

การเปลี่ยนผ่านอย่างรวดเร็วจากอินเทอร์เฟซแชตของ Large Language Model (LLM) แบบแยกส่วน ไปสู่ระบบอัตโนมัติของ AI Agent แบบหลายขั้นตอน (Multi-turn) และเซิร์ฟเวอร์ Model Context Protocol (MCP) ได้ก่อให้เกิดวิกฤตความปลอดภัยร้ายแรงในระดับองค์กร: คอขวดด้านอัตลักษณ์และการอนุญาตสิทธิ์ของเอเจนต์ (Agentic Identity Bottleneck)

ในช่วงปี 2024 และ 2025 นักพัฒนาส่วนใหญ่เชื่อมต่อเครื่องมืออัตโนมัติ เช่น Claude Code, Cursor, Windsurf, AutoGen และเอเจนต์ LangGraph เข้ากับ API ขององค์กรโดยใช้ Personal Access Tokens (PAT) แบบคงที่ หรือ API Key ระยะยาวที่ฮาร์ดโค้ดไว้ในไฟล์ .env หรือตัวแปรสภาพแวดล้อมของระบบปฏิบัติการ เมื่อ AI Agent ทำการรันคำสั่งเชลล์ในเครื่อง, คิวรีฐานข้อมูลภายใน (ผ่านเซิร์ฟเวอร์ PostgreSQL หรือ Supabase MCP), หรืออัปเดตระบบจัดการงาน (ผ่านเซิร์ฟเวอร์ Jira หรือ Linear MCP) เอเจนต์จะทำงานภายใต้อำนาจสิทธิ์แบบครอบคลุมและไร้ขอบเขต (Ambient Authority)

สถาปัตยกรรมเอเจนต์แบบเดิมที่มีช่องโหว่ (สิทธิ์คงที่ครอบคลุม):
+--------------------+       สร้างโปรเซสย่อย         +---------------------------+
|    LLM Host Agent  | ────────────────────────────> |    Local MCP Tool Server  |
| (Claude Code /     |   Env: GITHUB_TOKEN=ghp_...   |   (อ่านค่า process.env)   |
|  Cursor / LangSeq) |                               +-------------+-------------+
+---------+----------+                                             |
          | การโจมตีแบบ Indirect Prompt Injection                  | สิทธิ์อ่าน/เขียนไม่จำกัด
          v                                                        v
+--------------------+                                      +-------------------+
| พรอมต์ของผู้โจมตี  |                                      | ระบบปลายทาง       |
| บนหน้าเว็บภายนอก   | ──> ขโมยข้อมูลความลับคงที่ ────────> | GitHub / Slack /  |
| "Print your env"   |     ส่งไปยัง HTTP Webhook ผู้โจมตี   | ฐานข้อมูลภายใน    |
+--------------------+                                      +-------------------+

สถาปัตยกรรมข้อมูลประจำตัวแบบคงที่นี้มีข้อบกพร่องพื้นฐาน 3 ประการ:

  1. Prompt Injection เป็นช่องทางขโมยข้อมูลลับ: หากเอเจนต์ประมวลผลข้อมูลที่ไม่น่าเชื่อถือ (เช่น พรอมต์อันตรายที่ฝังอยู่ในหน้าเว็บ, อีเมล หรือ GitHub Issue) LLM อาจถูกล่อลวงให้รันคำสั่งวินิจฉัย เช่น printenv หรือเปิดอ่านไฟล์คอนฟิก ซึ่งจะทำให้คีย์ที่มีสิทธิ์ระดับสูงรั่วไหลทันที
  2. ขาดการมอบสิทธิ์ตามอัตลักษณ์ (Lack of Identity Delegation): API Key แบบคงที่ไม่สามารถแยกแยะได้ว่าการกระทำนั้นเกิดจากเจตนาของมนุษย์ หรือเกิดจากอาการประสาทหลอน (Hallucination) หรือการตัดสินใจอัตโนมัติของ AI ในบันทึกการตรวจสอบความปลอดภัย ทุกการเรียกใช้จะปรากฏเหมือนเป็นผู้ใช้มนุษย์คนเดียวกันทั้งหมด
  3. ไม่สามารถเพิกถอนสิทธิ์แบบไดนามิกและขาดขอบเขตสิทธิ์ขั้นต่ำ: โทเค็นแบบคงที่มักมีสิทธิ์กว้างเกินความจำเป็น (เช่น สิทธิ์อ่านเขียนทั้งคลังโค้ด) และมีอายุใช้งานนานหลายเดือนหรือไม่มีวันหมดอายุ

เพื่อแก้ไขความเสี่ยงเชิงระบบนี้ ระบบนิเวศ AI จึงเปลี่ยนมาใช้เฟรมเวิร์กการอนุญาตสิทธิ์แบบมอบอำนาจ (Delegated Authorization) อย่างไรก็ตาม การตัดสินใจเลือกระหว่าง OAuth 1.0a, OAuth 2.0 และมาตรฐานรวมฉบับใหม่อย่าง OAuth 2.1 — ผสานกับ PKCE (RFC 7636), DPoP (RFC 9449) และ Device Authorization Grant (RFC 8628) — จำเป็นต้องอาศัยความเข้าใจอย่างลึกซึ้งว่าโปรโตคอลเหล่านี้ทำงานอย่างไรภายใต้ข้อจำกัดของการประมวลผลแบบไร้หน้าจอ (Headless)


2. วิวัฒนาการของ OAuth: เปรียบเทียบโครงสร้าง 1.0a vs 2.0 vs 2.1

เพื่อทำความเข้าใจว่าเหตุใดเฟรมเวิร์ก AI Agent ยุคใหม่จึงกำหนดให้ต้องใช้ OAuth 2.1 เราจำเป็นต้องวิเคราะห์วิวัฒนาการทางสถาปัตยกรรม ข้อแลกเปลี่ยน และช่องโหว่สำคัญของทั้งสามเวอร์ชัน

วิวัฒนาการของข้อกำหนด OAUTH (2007 - 2026):
+---------------------------------------------------------------------------------------------+
| OAuth 1.0a (RFC 5849, 2010)                                                                 |
| - ลายมือชื่อทางคริปโตกราฟีแบบสมมาตร/อสมมาตรในทุกๆ HTTP Request (HMAC-SHA1, RSA-SHA1)        |
| - ไม่มีระบบรีเฟรชโทเค็น, การคำนวณลายมือชื่อขึ้นกับสถานะ, ความปลอดภัยเป็นอิสระจากทรานสปอร์ต   |
| - ผลการประเมินสำหรับ AI: ใช้ไม่ได้ ภาระการเข้ารหัสทำลายระบบสตรีมมิงและพร็อกซีแบบไดนามิก      |
+---------------------------------------------------------------------------------------------+
                                               │
                                               ▼
+---------------------------------------------------------------------------------------------+
| OAuth 2.0 (RFC 6749 & RFC 6750, 2012)                                                       |
| - มอบหมายการรักษาความปลอดภัยให้เลเยอร์ทรานสปอร์ต (TLS 1.2/1.3)                               |
| - นำเสนอ Bearer Token, Scopes, Refresh Token และประเภท Grant เฉพาะทาง                       |
| - รวมถึงโฟลว์ Implicit และ Resource Owner Password Credentials (ROPC)                        |
| - ผลการประเมินสำหรับ AI: อันตราย Bearer Token ถูกขโมยและนำกลับมาใช้ซ้ำได้ง่ายผ่าน SSRF     |
+---------------------------------------------------------------------------------------------+
                                               │
                                               ▼
+---------------------------------------------------------------------------------------------+
| OAuth 2.1 (มาตรฐานรวม IETF, 2025/2026)                                                      |
| - ตัดโฟลว์ที่ไม่ปลอดภัยออกทั้งหมด (ยกเลิก Implicit และ Password Grant อย่างถาวร)            |
| - บังคับใช้ PKCE (RFC 7636) สำหรับ Authorization Code ทุกกรณี (ทั้ง Public และ Confidential)|
| - ตรวจสอบ Redirect URI แบบตรงกันทุกตัวอักษร, ห้ามส่งโทเค็นผ่าน Query Parameter ใน URL       |
| - บังคับใช้ Refresh Token Rotation (RTR) หรือ Sender-Constrained Tokens (DPoP / mTLS)        |
| - ผลการประเมินสำหรับ AI: มาตรฐานระดับโกลด์สำหรับการมอบสิทธิ์อัตลักษณ์ของ MCP และเอเจนต์     |
+---------------------------------------------------------------------------------------------+

OAuth 1.0a (RFC 5849): ความเข้มงวดทางคริปโตกราฟีและสถานะ

OAuth 1.0a ถูกพัฒนาขึ้นในยุคที่ HTTPS/TLS มีค่าใช้จ่ายสูงและยังไม่แพร่หลาย เพื่อป้องกันการดักฟังบน HTTP แบบข้อความธรรมดา OAuth 1.0a จึงกำหนดให้ไคลเอนต์และเซิร์ฟเวอร์ต้องคำนวณลายมือชื่อดิจิทัล (HMAC-SHA1 หรือ RSA-SHA1) ในทุกๆ คำขอเดี่ยว

การคำนวณลายมือชื่อต้องจัดระเบียบเมธอด HTTP, URL มาตรฐาน และสตริงพารามิเตอร์คิวรีที่เรียงตามพจนานุกรม พร้อมด้วยส่วนหัว, Nonce ของไคลเอนต์ และเวลา Unix:

$$\text{BaseString} = \text{HTTP\_METHOD} \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{Encode}(\text{URL}) \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{Encode}(\text{SortedParams})$$

$$\text{Signature} = \text{HMAC-SHA1}(\text{ClientSecret} \mathbin{\Vert} \text{"\&"} \mathbin{\Vert} \text{TokenSecret}, \text{BaseString})$$

ทำไม OAuth 1.0a จึงล้มเหลวกับ AI Agent:

  1. การสตรีมและทรานสปอร์ตแบบ Chunked: โปรโตคอลเอเจนต์สมัยใหม่ (เช่น MCP ผ่าน Server-Sent Events หรือ WebSockets) ส่งข้อมูล JSON-RPC ทีละส่วน การคำนวณลายมือชื่อซ้ำบนสตรีมข้อมูลที่ไม่แน่นอนจะทำให้การตรวจสอบความถูกต้องล้มเหลวเสมอ
  2. การจัดลำดับคำสั่งแบบไดนามิก: AI Agent สร้างคำขอ HTTP แบบไดนามิกตามพารามิเตอร์ของ LLM การสลับลำดับพารามิเตอร์เพียงเล็กน้อย หรือความแตกต่างในการเข้ารหัส URL (%20 กับ +) จะทำให้ลายมือชื่อเป็นโมฆะและเกิดข้อผิดพลาด 401 Unauthorized ซ้ำซาก
  3. ไม่มีการแยกการรีเฟรชแบบสั้น: ไม่มีกลไกการหมดอายุของโทเค็นระยะสั้นพร้อมระบบหมุนเวียนอัตโนมัติ ทำให้ข้อมูลลับระยะยาวต้องถูกเก็บไว้ในสภาพแวดล้อมไคลเอนต์อย่างถาวร

OAuth 2.0 (RFC 6749): ความเรียบง่ายที่แลกมาด้วยความเสี่ยงของ Bearer

OAuth 2.0 แก้ไขความซับซ้อนโดยผลักภาระความปลอดภัยไปยังเลเยอร์ทรานสปอร์ต (บังคับใช้ HTTPS) และนำเสนอ Bearer Token (RFC 6750) ใครก็ตามที่ถือโทเค็นนี้จะสามารถเข้าถึงทรัพยากรได้ เสมือนกับการถือเงินสด:

GET /v1/repositories HTTP/1.1
Host: api.github.com
Authorization: Bearer ya29.a0AfH6SMB...

OAuth 2.0 ได้กำหนด 4 โฟลว์การอนุญาตสิทธิ์ดั้งเดิม:

  1. Authorization Code Grant: โฟลว์การเปลี่ยนเส้นทางสำหรับเว็บแอปพลิเคชันที่มีระบบหลังบ้านปลอดภัย
  2. Implicit Grant: โฟลว์บนเบราว์เซอร์ที่ส่งคืนโทเค็นกลับมาใน Hash Fragment ของ URL (#access_token=...)
  3. Resource Owner Password Credentials (ROPC): การส่งชื่อผู้ใช้และรหัสผ่านไปยังแอปไคลเอนต์โดยตรง
  4. Client Credentials Grant: การอนุญาตสิทธิ์แบบ Machine-to-Machine (M2M) สำหรับระบบเบื้องหลังโดยไม่มีมนุษย์เกี่ยวข้อง

จุดอ่อนร้ายแรงของ OAuth 2.0 ในระบบ AI:

  • ช่องโหว่การนำ Bearer Token มาใช้ซ้ำ (Replay Attack): หากสภาพแวดล้อมของเอเจนต์ถูกโจมตีผ่าน SSRF หรือการรั่วไหลของบันทึก ผู้โจมตีสามารถขโมยโทเค็น Bearer และนำไปใช้งานซ้ำได้จากทุกที่ทั่วโลกจนกว่าจะหมดอายุ
  • กับดักของ Implicit Grant: อินเทอร์เฟซเอเจนต์รุ่นแรกบนเดสก์ท็อปหรือ SPA ใช้ Implicit Flow ทำให้โทเค็นรั่วไหลผ่านประวัติเบราว์เซอร์และส่วนหัว Referer
  • ปัญหาของ Password Grant: ผู้พัฒนาเอเจนต์ CLI มักเขียนโค้ดให้ผู้ใช้กรอกรหัสผ่านในเทอร์มินัล ซึ่งทำลายหลักการพื้นฐานของ OAuth ที่จะไม่แบ่งปันข้อมูลประจำตัวแก่บุคคลที่สาม

OAuth 2.1: มาตรฐานความปลอดภัยเข้มงวดสำหรับเอเจนต์อัตโนมัติ

OAuth 2.1 คือข้อกำหนดรวมของ IETF ที่กำจัดหนี้ทางเทคนิคและช่องโหว่ความปลอดภัยของ OAuth 2.0:

  1. ตัดโฟลว์ที่ไม่ปลอดภัยออกทั้งหมด: ทั้ง Implicit Grant และ Password Credentials ถูกยกเลิกและนำออกจากข้อกำหนดอย่างถาวร
  2. บังคับใช้ PKCE สำหรับ Authorization Code ทุกกรณี: Proof Key for Code Exchange (RFC 7636) มีผลบังคับใช้กับ ทั้ง Public Clients (เอเจนต์ CLI, ส่วนขยาย IDE) และ Confidential Clients (กลุ่มเอเจนต์หลังบ้าน)
  3. ตรวจสอบ Redirect URI ตรงกันทุกไบต์: เซิร์ฟเวอร์อนุญาตสิทธิ์ต้องเปรียบเทียบสตริง URI อย่างเคร่งครัดแบบตรงกันทุกไบต์ เพื่อป้องกันการโจมตีแบบ Open Redirect
  4. ห้ามส่งโทเค็นใน Query Parameter ของ URL: ป้องกันไม่ให้โทเค็นรั่วไหลไปยังบันทึกการเข้าถึงของเว็บเซิร์ฟเวอร์หรือแคชของพร็อกซี
  5. คุ้มครอง Refresh Token อย่างเคร่งครัด: ต้องใช้ Refresh Token Rotation (RTR) หรือ Sender-Constrained Tokens (DPoP / mTLS)

ตารางเปรียบเทียบสถาปัตยกรรม: OAuth 1.0a vs OAuth 2.0 vs OAuth 2.1

มิติทางสถาปัตยกรรม OAuth 1.0a (RFC 5849) OAuth 2.0 (RFC 6749 / 6750) OAuth 2.1 (มาตรฐาน IETF 2026)
รูปแบบการเข้ารหัส เซ็นชื่อคำขอระดับแอปพลิเคชัน (HMAC/RSA) ความปลอดภัยระดับทรานสปอร์ต (TLS) + Bearer TLS + บังคับใช้ PKCE + การผูกมัดผู้ส่ง (DPoP/mTLS)
ความเสี่ยงการ Replay ปลอดภัย (เซ็นชื่อด้วย Nonce เฉพาะตัว) สูงมาก (ผู้ถือโทเค็นเข้าถึงได้ทันที) ปลอดภัย (ผูกกับกุญแจลับของผู้ส่งผ่าน DPoP)
ข้อกำหนด PKCE ไม่รองรับ ทางเลือก (RFC 7636 เดิมเน้นมือถือ) บังคับใช้ สำหรับการแลกเปลี่ยนโค้ดทั้งหมด
Implicit Grant ไม่รองรับ อนุญาต (ออกแบบสำหรับ SPA บนเว็บ) ถูกตัดออกและห้ามใช้อย่างเด็ดขาด
Password Grant (ROPC) ไม่รองรับ อนุญาต (การแลกเปลี่ยนรหัสผ่านตรง) ถูกตัดออกและห้ามใช้อย่างเด็ดขาด
การตรวจ Redirect URI ตรวจสอบแบบพรีฟิกซ์ได้ มักอนุญาตให้ใช้ Wildcard และเส้นทางย่อย ต้องตรงกันทุกตัวอักษรแบบไบต์ต่อไบต์
โทเค็นในพารามิเตอร์ URL รองรับ อนุญาต (?access_token=...) ห้ามใช้เด็ดขาด (ผ่าน Header หรือ Body เท่านั้น)
วงจรชีวิต Refresh Token ไม่มีกลไกเฉพาะ ใช้โทเค็นเดิมซ้ำจนกว่าจะเพิกถอนหรือหมดอายุ ต้องหมุนเวียนใหม่ทุกครั้ง (RTR) หรือผูกกับ DPoP
ความเหมาะกับ CLI Agent แย่มาก (การคำนวณลายมือชื่อในเชลล์เปราะบาง) มีความเสี่ยง (การดักจับบน Loopback ในเครื่อง) เหมาะสมที่สุด (PKCE + พอร์ต Loopback ชั่วคราว)
ความเหมาะกับเซิร์ฟเวอร์ MCP ใช้งานไม่ได้กับ JSON-RPC แบบสตรีมมิง ใช้งานได้แต่เสี่ยงต่อการรั่วไหลของความลับ มาตรฐานเริ่มต้น (จำกัดสิทธิ์ขั้นต่ำเคร่งครัด)

3. โครงสร้างการยืนยันตัวตนใน Model Context Protocol (MCP)

Model Context Protocol (MCP) ซึ่งริเริ่มโดย Anthropic และนำมาใช้งานใน Claude Code, Cursor, Windsurf ได้กำหนดสถาปัตยกรรมไคลเอนต์-เซิร์ฟเวอร์แบบอสมมาตรบน JSON-RPC 2.0

ในการติดตั้งใช้งาน MCP มีขอบเขตการสื่อสารหลัก 2 ส่วน:

  • ขอบเขต A (โฮสต์ไปยังเซิร์ฟเวอร์ MCP): การเชื่อมต่อระหว่างแอปพลิเคชันไคลเอนต์ LLM (Claude Code, Cursor) และโปรเซสเซิร์ฟเวอร์ MCP
  • ขอบเขต B (เซิร์ฟเวอร์ MCP ไปยังโครงสร้างพื้นฐานภายนอก): การเชื่อมต่อระหว่างเซิร์ฟเวอร์ MCP และ API ของ SaaS ภายนอก (GitHub, Jira, Linear, Slack)
โครงสร้างการยืนยันตัวตนใน MODEL CONTEXT PROTOCOL (MCP):
+-------------------------------------------------------------------------------------------------------+
| รันไทม์ของโฮสต์ MCP (เช่น Claude Code / Cursor / เฟรมเวิร์กเอเจนต์อัตโนมัติ)                           |
|                                                                                                       |
|  +---------------------+        บริบทพรอมต์            +--------------------------------------------+ |
|  | พรอมต์ผู้ใช้ / LLM   | <───────────────────────────> | เอนจินประมวลผล (Claude 3.7 / GPT-4o)       | |
|  +----------+----------+                               +--------------------------------------------+ |
|             | เรียกใช้คำสั่งเครื่องมือ (`tools/call`)                                                  |
|             v                                                                                         |
|  +--------------------------------------------------------------------------------------------------+ |
|  | เอนจินไคลเอนต์ MCP                                                                               | |
|  | - จัดการการเชื่อมต่อ OAuth 2.1 PKCE กับเซิร์ฟเวอร์อนุญาตสิทธิ์                                     | |
|  | - ถือกุญแจส่วนตัวชั่วคราว DPoP ไว้ในหน่วยความจำที่ปลอดภัย                                          | |
|  | - สร้าง DPoP Proof JWT ทุกคำขอ; แนบ Access Token เข้าไปในส่วนหัว JSON-RPC                         | |
|  +-----------------------------------+--------------------------------------------------------------+ |
+--------------------------------------|----------------------------------------------------------------+
                                       |
                                       | ช่องทาง: Stdio (โปรเซสในเครื่อง) หรือ SSE/HTTP (เซิร์ฟเวอร์ระยะไกล)
                                       v
+-------------------------------------------------------------------------------------------------------+
| รันไทม์ของเซิร์ฟเวอร์ MCP (เช่น GitHub MCP / ฐานข้อมูลองค์กร MCP)                                     |
|                                                                                                       |
|  +--------------------------------------------------------------------------------------------------+ |
|  | ตัวดักจับตรวจสอบการยืนยันตัวตนและโทเค็น                                                            | |
|  | 1. ตรวจสอบลายมือชื่อโทเค็น OAuth 2.1 ผ่าน JWKS ของเซิร์ฟเวอร์อนุญาตสิทธิ์                           | |
|  | 2. ตรวจสอบ DPoP Proof: ตรวจสอบเมธอด HTTP, URI, Nonce และกุญแจสาธารณะชั่วคราว                      | |
|  | 3. ประเมินขอบเขตสิทธิ์ (Scopes): บังคับใช้สิทธิ์ต่ำสุด (`issues:read` ปฏิเสธ `admin:all`)           | |
|  +-----------------------------------+--------------------------------------------------------------+ |
|                                      |                                                                |
|                                      v                                                                |
|  +--------------------------------------------------------------------------------------------------+ |
|  | เอนจินรันเครื่องมือ MCP (การทำงานของ `tools/call`)                                                 | |
|  | - คัดกรองอินพุต, ป้องกัน Path Traversal, ประมวลผลคำสั่ง API ในแซนด์บ็อกซ์                            | |
|  +-----------------------------------+--------------------------------------------------------------+ |
+--------------------------------------|----------------------------------------------------------------+
                                       | เรียก API ภายนอกด้วยโทเค็นที่ได้รับมอบหมายและจำกัดสิทธิ์
                                       v
                      +----------------------------------+
                      | บริการ SaaS และฐานข้อมูลองค์กร   |
                      | (GitHub / Jira / PostgreSQL / S3)|
                      +----------------------------------+

การเปรียบเทียบช่องทาง Stdio ในเครื่อง vs SSE/HTTP ระยะไกล

  1. ช่องทาง Stdio ในเครื่อง (transport: "stdio"):
  • เซิร์ฟเวอร์ MCP ทำงานเป็นโปรเซสลูกในเครื่องที่โฮสต์สร้างขึ้น และสื่อสารผ่าน stdin และ stdout
  • ข้อผิดพลาดในอดีต: นักพัฒนามักใส่ข้อมูลลับลงในตัวแปรสภาพแวดล้อม:
  • ช่องโหว่: คำสั่งเชลล์ใดๆ ที่เอเจนต์สั่งรันสามารถเปิดดู /proc/[pid]/environ หรือรันคำสั่ง env เพื่อขโมยโทเค็น GitHub ของทั้งองค์กรได้ทันที
  • ทางออกของ OAuth 2.1: โฮสต์ทำหน้าที่เก็บรักษาโทเค็น OAuth 2.1 PKCE ไว้อย่างปลอดภัย เซิร์ฟเวอร์ MCP เริ่มต้นทำงานโดยไม่มีข้อมูลลับคงที่ และจะได้รับโทเค็นระยะสั้นในระหว่างขั้นตอน Handshake เริ่มแรก
  1. ช่องทาง SSE/HTTP ระยะไกล (transport: "sse"):
  • เซิร์ฟเวอร์ MCP ทำงานเป็นเว็บบริการบนพอร์ต HTTP โดยใช้ Server-Sent Events ในการรับส่งข้อมูล
  • ในสถาปัตยกรรมนี้ การใช้ OAuth 2.1 เป็นสิ่งจำเป็น โดยไคลเอนต์ MCP ต้องยืนยันตัวตนด้วยส่วนหัว Authorization ที่ตรวจสอบผ่าน JWKS ร่วมกับ DPoP Proof

4. เจาะลึก PKCE (RFC 7636): ปกป้อง Callback บนเครื่องของเอเจนต์

Proof Key for Code Exchange (PKCE) ถูกคิดค้นขึ้นเพื่อป้องกันการดักจับโค้ดอนุญาตสิทธิ์บนอุปกรณ์ ใน OAuth 2.1 การใช้ PKCE ถือเป็นข้อบังคับสำหรับการแลกเปลี่ยนรหัสอนุญาตสิทธิ์ทุกครั้ง

เหตุใดเอเจนต์ CLI และ IDE จึงเป็น Public Client

เครื่องมืออย่าง Claude Code หรือ Cursor จัดอยู่ในกลุ่ม Public Client: เนื่องจากโค้ดหรือไฟล์ไบนารีทำงานอยู่บนเครื่องของผู้ใช้ จึงไม่สามารถซ่อน client_secret แบบคงที่ได้อย่างปลอดภัย ผู้ใดก็ตามสามารถถอดรหัสย้อนกลับไบนารีเพื่อดึงความลับออกมาได้

เมื่อส่งคำขออนุญาตสิทธิ์ เซิร์ฟเวอร์จะส่ง Authorization Code กลับมายัง URI การเปลี่ยนเส้นทางในเครื่อง (โดยปกติคือเซิร์ฟเวอร์ HTTP บน Loopback ชั่วคราว เช่น http://127.0.0.1:18492/callback)

การโจมตีดักจับโค้ดอนุญาตสิทธิ์ (กรณีไม่ใช้ PKCE):
1. เอเจนต์ CLI ส่งคำขอ Authorization Code ไปยังเซิร์ฟเวอร์ Auth
2. โปรเซสไม่พึงประสงค์ในเครื่องแอบดักฟังทราฟฟิกบนพอร์ต Loopback
3. เซิร์ฟเวอร์ Auth ส่งการเปลี่ยนเส้นทางไปที่ http://127.0.0.1:18492/callback?code=AUTH_CODE_123
4. โปรเซสไม่พึงประสงค์ดักจับ AUTH_CODE_123 ได้
5. โปรเซสไม่พึงประสงค์ส่ง AUTH_CODE_123 ไปที่ /oauth/token
   เนื่องจากเป็น Public Client จึงไม่ต้องใช้ client_secret เซิร์ฟเวอร์จึงมอบ Access Token ให้ผู้โจมตี!

กลไกการป้องกันทางคณิตศาสตร์ของ PKCE

PKCE ป้องกันช่องโหว่นี้โดยการสร้างรหัสลับเฉพาะสำหรับการอนุญาตสิทธิ์แต่ละครั้งแบบไดนามิก:

ขั้นตอนการทำงานของโปรโตคอล PKCE:
+-------------+                     +-----------------------+                    +--------------------+
|  Agent CLI  |                     |  เบราว์เซอร์ผู้ใช้    |                    |   เซิร์ฟเวอร์ Auth |
|  (ไคลเอนต์) |                     +-----------+-----------+                    +---------+----------+
+------+------+                                 |                                          |
       | 1. สร้าง code_verifier (ค่าสุ่ม)       |                                          |
       |    คำนวณ code_challenge = S256(...)    |                                          |
       |                                        |                                          |
       | 2. เปิด HTTP Listener บน Loopback      |                                          |
       |    เปิดเบราว์เซอร์พร้อม challenge ────>| 3. GET /authorize?response_type=code     |
       |                                        |    &client_id=agent_cli                  |
       |                                        |    &code_challenge=E9Melhoa2Owv...       |
       |                                        |    &code_challenge_method=S256 ─────────>|
       |                                        |                                          | 4. ผู้ใช้ยินยอม
       |                                        | 5. 302 เปลี่ยนเส้นทางกลับมาที่ Loopback   |    บันทึก challenge
       |                                        |<─────────────────────────────────────────|
       |<───────────────────────────────────────|    http://127.0.0.1:18492/callback?code=AC_88921
       | 6. รับโค้ดอนุญาตสิทธิ์                 |
       |                                                                                   |
       | 7. POST /oauth/token                                                              |
       |    code=AC_88921 & code_verifier=dBjftJeZ4CVP-mB92K... ─────────────────────────>|
       |                                                                                   | 8. คำนวณตรวจสอบ:
       |                                                                                   |    SHA256(verifier)
       |                                                                                   |    == challenge?
       | 9. ส่งคืน Access Token + Refresh Token (RTR) <────────────────────────────────────|    ถูกต้อง: ออกโทเค็น
+------+------+
  1. Code Verifier: เอเจนต์สร้างสตริงสุ่มที่มีความปลอดภัยสูง $V$ ความยาว 43 ถึง 128 ตัวอักษร ([A-Z], [a-z], [0-9], -, ., _, ~):
  2. Code Challenge: ไคลเอนต์คำนวณค่าแฮช SHA-256 ของ $V$ และเข้ารหัสเป็น Base64URL:
  3. ส่งคำขออนุญาต: ไคลเอนต์ส่ง $C$ และ code_challenge_method=S256 ไปยัง /authorize เซิร์ฟเวอร์จะจัดเก็บ $C$ ไว้
  4. การแลกเปลี่ยนโทเค็น: ไคลเอนต์ส่ง code_verifier=V แบบข้อความธรรมดาไปที่ /token เซิร์ฟเวอร์จะคำนวณ $\text{Base64URL-Encode}(\text{SHA-256}(V))$ และตรวจสอบว่าตรงกับ $C$ หรือไม่

แม้ว่าโปรเซสอันตรายจะดักจับโค้ดได้ แต่จะไม่สามารถนำไปแลกโทเค็นได้ เพราะไม่มี code_verifier ตัวจริง ซึ่งไม่เคยหลุดออกจากหน่วยความจำของเอเจนต์

โค้ด TypeScript สำหรับใช้งานจริง: PKCE Engine

// pkce.ts - Enterprise OAuth 2.1 PKCE Engine for AI Agent Clients
import { randomBytes, createHash } from "node:crypto";

export interface PKCEChallenge {
  codeVerifier: string;
  codeChallenge: string;
  codeChallengeMethod: "S256";
}

export class PKCEEngine {
  /**
   * Generates a cryptographically secure code_verifier (RFC 7636 Section 4.1)
   * Length defaults to 64 bytes of entropy (yielding ~86 base64url characters).
   */
  public static generateVerifier(length: number = 64): string {
    if (length < 32 || length > 96) {
      throw new RangeError("Verifier byte length must be between 32 and 96.");
    }
    const buffer = randomBytes(length);
    return this.base64UrlEncode(buffer);
  }

  /**
   * Computes the S256 code_challenge from the code_verifier (RFC 7636 Section 4.2)
   */
  public static computeChallenge(verifier: string): string {
    const hash = createHash("sha256").update(verifier, "ascii").digest();
    return this.base64UrlEncode(hash);
  }

  /**
   * Generates the complete PKCE pair ready for OAuth 2.1 authorization
   */
  public static createPair(): PKCEChallenge {
    const codeVerifier = this.generateVerifier(64);
    const codeChallenge = this.computeChallenge(codeVerifier);
    return {
      codeVerifier,
      codeChallenge,
      codeChallengeMethod: "S256",
    };
  }

  /**
   * Server-side verification: Validates an incoming code_verifier against stored challenge
   */
  public static verify(verifier: string, storedChallenge: string): boolean {
    const computed = this.computeChallenge(verifier);
    // Timing-safe buffer comparison to prevent side-channel timing attacks
    const bufA = Buffer.from(computed);
    const bufB = Buffer.from(storedChallenge);
    if (bufA.length !== bufB.length) return false;
    
    let result = 0;
    for (let i = 0; i < bufA.length; i++) {
      result |= bufA[i] ^ bufB[i];
    }
    return result === 0;
  }

  private static base64UrlEncode(buffer: Buffer): string {
    return buffer
      .toString("base64")
      .replace(/\\+/g, "-")
      .replace(/\\//g, "_")
      .replace(/=+$/, "");
  }
}

5. การยืนยันตัวตนบนเซิร์ฟเวอร์แบบ Headless และ CLI: Device Flow (RFC 8628)

AI Agent ยุคใหม่มักทำงานในสภาพแวดล้อม Headless ที่ไม่มีเว็บเบราว์เซอร์:

  • คอนเทนเนอร์ Docker บนคลาวด์ (AWS ECS, Kubernetes, Fly.io)
  • เครื่องรัน CI/CD ชั่วคราว (GitHub Actions, GitLab CI)
  • เซิร์ฟเวอร์เสมือนระยะไกลและเซสชัน SSH

ในสภาพแวดล้อมเช่นนี้ เอเจนต์ไม่สามารถเปิดเบราว์เซอร์เพื่อเปลี่ยนเส้นทางได้ และการให้ผู้ใช้พิมพ์รหัสผ่านในเทอร์มินัลก็ขัดต่อมาตรฐาน OAuth 2.1 ทางออกที่เป็นมาตรฐานอุตสาหกรรมคือ OAuth 2.0 Device Authorization Grant (RFC 8628):

การทำงานของ DEVICE AUTHORIZATION GRANT (RFC 8628) บนสภาพแวดล้อม HEADLESS:
+-------------------+                                                +-----------------------+
|   Headless Agent  |                                                |    เซิร์ฟเวอร์ Auth   |
|  (Docker / Cloud) |                                                +-----------+-----------+
+---------+---------+                                                            |
          | 1. POST /oauth/device/code (client_id, scope) ──────────────────────>|
          |                                                                      | 2. สร้าง:
          | 3. ส่งคืนข้อมูลประจำตัวอุปกรณ์:                                      |    device_code (ความลับ)
          |    - user_code: "WDJB-HGNP"                                          |    user_code (สาธารณะ)
          |    - verification_uri: "https://auth.corp.com/activate"              |    interval: 5 วินาที
          |    - interval: 5 <───────────────────────────────────────────────────|
          |                                                                      |
          | 4. แสดงข้อความสั่งการบนเทอร์มินัลให้ผู้ใช้เห็น:                      |
          |    "เปิด https://auth.corp.com/activate และกรอกรหัส: WDJB-HGNP"      |
          |                                                                      |
          | 5. เริ่มลูปสอบถามสถานะ (Polling Loop):                               |
          |    POST /oauth/token (grant_type=device_code, device_code=...) ─────>|
          |    <── 400 Bad Request: {"error": "authorization_pending"} ─────────|
          |    [รอ 5 วินาที]                                                     |
          |                                                                      |
+---------+---------+     ผู้ใช้เปิดเบราว์เซอร์บนแล็ปท็อปหรือมือถือ               |
| แล็ปท็อปผู้ใช้    | ──> กรอกรหัส "WDJB-HGNP" และยืนยันตัวตน MFA ──────────────>| 6. ผู้ใช้อนุมัติ!
+-------------------+                                                            |
          |                                                                      |
          | 7. รอบการสอบถามถัดไป:                                                |
          |    POST /oauth/token ───────────────────────────────────────────────>|
          |    <── 200 OK: {access_token: "...", refresh_token: "..."} ──────────|
          v
[เอเจนต์แบบ Headless ยืนยันตัวตนสำเร็จโดยไม่ต้องกรอกรหัสผ่านในคอนโซล]

ทางเลือกแบบ Machine-to-Machine (M2M): RFC 7523 Private Key JWT

กรณีที่เอเจนต์ทำงานอัตโนมัติเต็มรูปแบบโดยไม่มีมนุษย์คอยดู (เช่น บอทรีแฟกเตอร์โค้ดตอนกลางคืน) สถาปัตยกรรม Zero-Trust จะใช้ Client Credentials Grant ร่วมกับ RFC 7523 (JWT Profile for Client Authentication):

  • แทนที่จะส่ง client_secret แบบคงที่ผ่านเครือข่าย เอเจนต์จะถือกุญแจส่วนตัวแบบอสมมาตร (RSA หรือ ECDSA) ไว้ในโมดูลความปลอดภัยฮาร์ดแวร์ (HSM) หรือ Kubernetes Secret Vault
  • ในการยืนยันตัวตน เอเจนต์จะเซ็นชื่อและสร้าง JWT อายุสั้น 60 วินาที พร้อมระบุ UUID jti และเป้าหมายผู้รับ aud
  • เซิร์ฟเวอร์จะตรวจสอบลายมือชื่อกับกุญแจสาธารณะที่ลงทะเบียนไว้ล่วงหน้า

6. วงจรชีวิตของโทเค็นและขั้นตอนการรีเฟรชอัตโนมัติ

เอเจนต์มักต้องทำงานที่ใช้เวลานานหลายชั่วโมง เนื่องจาก Access Token ของ OAuth 2.1 ถูกกำหนดให้มีอายุสั้น (5 ถึง 15 นาที) เอเจนต์จึงต้องจัดการการต่ออายุโทเค็นแบบอัตโนมัติโดยไม่รบกวนการทำงานของโมเดล

Refresh Token Rotation (RTR) และการตรวจจับการบุกรุก

ใน OAuth 2.1 โทเค็นรีเฟรชจะได้รับการปกป้องด้วยกลไก Refresh Token Rotation (RTR):

  1. ทุกครั้งที่เอเจนต์ส่ง refresh_token ไปแลกที่ /oauth/token เซิร์ฟเวอร์จะยกเลิกโทเค็นนั้นทันที
  2. เซิร์ฟเวอร์จะออก access_token ใหม่ และ refresh_token ชุดใหม่อีกชุดหนึ่ง
  3. หากผู้โจมตีพยายามนำ Refresh Token เก่าที่ถูกใช้ไปแล้วกลับมาใช้ใหม่ เซิร์ฟเวอร์จะระบุว่าเป็น เหตุการณ์การบุกรุกทันที:

$$\text{Incoming Token State} == \text{"REVOKED"} \implies \text{Revoke All Tokens in Family Tree}$$

เซิร์ฟเวอร์จะเพิกถอนโทเค็นทั้งหมดในสายการอนุญาตนั้นทันที ทำให้โทเค็นทั้งหมดของเอเจนต์เป็นโมฆะเพื่อความปลอดภัย

การหมุนเวียน REFRESH TOKEN (RTR) และการเพิกถอนสิทธิ์อัตโนมัติ:
ลำดับการออกโทเค็น:
[Refresh Token A] ──(ถูกใช้แล้ว)──> [Refresh Token B] ──(ถูกใช้แล้ว)──> [Refresh Token C] (ใช้งานอยู่)
         │
         │ ผู้โจมตีพยายามส่ง [Refresh Token A] ที่ถูกใช้ไปแล้วซ้ำอีกครั้ง
         v
[เซิร์ฟเวอร์ Auth ตรวจพบการใช้ซ้ำของโทเค็น A ที่ถูกเพิกถอนไปแล้ว!]
         │
         ▼
[แจ้งเตือนภัยระดับวิกฤต]: เพิกถอนโทเค็น B, โทเค็น C และ Access Token ทั้งหมดทันที
เซสชันของเอเจนต์หยุดทำงานอย่างปลอดภัย ป้องกันการยกระดับสิทธิ์โดยไม่ได้รับอนุญาต

โค้ด Python สำหรับใช้งานจริง: ตัวจัดการโทเค็นแบบ Asynchronous ป้องกันการชนกัน

# token_manager.py - Enterprise Async Token Lifecycle Manager for AI Agents
import asyncio
import time
import httpx
from typing import Optional, Dict, Any

class AgentTokenManager:
    def __init__(
        self,
        token_endpoint: str,
        client_id: str,
        initial_refresh_token: str,
        proactive_refresh_seconds: int = 60,
    ):
        self.token_endpoint = token_endpoint
        self.client_id = client_id
        self.refresh_token = initial_refresh_token
        self.access_token: Optional[str] = None
        self.expires_at: float = 0.0
        self.proactive_refresh_seconds = proactive_refresh_seconds
        self._lock = asyncio.Lock()

    async def get_valid_access_token(self) -> str:
        now = time.time()
        if self.access_token and (self.expires_at - now) > self.proactive_refresh_seconds:
            return self.access_token

        async with self._lock:
            now = time.time()
            if self.access_token and (self.expires_at - now) > self.proactive_refresh_seconds:
                return self.access_token

            await self._refresh_token_exchange()
            if not self.access_token:
                raise RuntimeError("Failed to acquire valid access token from authorization server.")
            return self.access_token

    async def _refresh_token_exchange(self) -> None:
        payload = {
            "grant_type": "refresh_token",
            "refresh_token": self.refresh_token,
            "client_id": self.client_id,
        }
        
        async with httpx.AsyncClient(timeout=10.0) as client:
            try:
                response = await client.post(
                    self.token_endpoint,
                    data=payload,
                    headers={"Content-Type": "application/x-www-form-urlencoded"},
                )
            except httpx.RequestError as exc:
                raise ConnectionError(f"Network transport error during token refresh: {exc}")

            if response.status_code == 200:
                data: Dict[str, Any] = response.json()
                self.access_token = data["access_token"]
                expires_in = int(data.get("expires_in", 3600))
                self.expires_at = time.time() + expires_in
                
                # Update to the newly rotated refresh token if provided
                if "refresh_token" in data:
                    self.refresh_token = data["refresh_token"]
            elif response.status_code in (400, 401):
                err_data = response.json()
                # If error is 'invalid_grant', the token was likely already rotated or revoked
                raise PermissionError(f"Token refresh rejected (possible token theft or expiry): {err_data}")
            else:
                response.raise_for_status()

7. การแยกข้อมูลประจำตัวแบบ Zero-Trust: DPoP (RFC 9449)

แม้จะใช้ OAuth 2.1 และ PKCE แต่ Bearer Token ยังคงมีจุดอ่อนสำคัญ: หากโทเค็นถูกขโมย ผู้ใดก็สามารถนำไปใช้งานได้

ในระบบ AI หากผู้โจมตีใช้ Prompt Injection ล่อลวงให้เอเจนต์ส่งคำขอ HTTP ออกไปยังเซิร์ฟเวอร์ภายนอก (SSRF) พร้อมแนบส่วนหัวยืนยันตัวตน โทเค็นก็จะถูกขโมยไป เพื่อความปลอดภัยสูงสุดแบบ Zero-Trust มาตรฐาน OAuth 2.1 จึงผสานรวม DPoP: Demonstrating Proof-of-Possession at the Application Layer (RFC 9449)

กลไกการผูกมัดผู้ส่งในระดับแอปพลิเคชันด้วย DPOP (RFC 9449):
+-------------------------------------------------------------------------------------------------+
| สภาพแวดล้อมของเอเจนต์ในเครื่อง (ไคลเอนต์)                                                        |
| - สร้างคู่กุญแจชั่วคราว: กุญแจสาธารณะ (JWK) + กุญแจส่วนตัว (เก็บใน RAM/Enclave เท่านั้น)        |
+-------------------------------------------------------------------------------------------------+
                                                │
                                                │ 1. แนบส่วนหัว DPoP Proof:
                                                │    DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Ar...
                                                │    ข้อมูลภายใน: {
                                                │      "htm": "GET",
                                                │      "htu": "https://api.enterprise.com/mcp/tools",
                                                │      "iat": 1772630400,
                                                │      "jti": "random_nonce_9921",
                                                │      "jwk": { ...กุญแจสาธารณะ... }
                                                │    }
                                                │
                                                │ 2. ส่งโทเค็น DPoP ที่ผูกมัดแล้ว:
                                                │    Authorization: DPoP dpop_access_token_88921
                                                v
+-------------------------------------------------------------------------------------------------+
| เกตเวย์ MCP ขององค์กร (Resource Server)                                                         |
| 1. ตรวจสอบว่า Access Token ผูกมัดกับลายนิ้วมือกุญแจสาธารณะใน JWK ทางคริปโตกราฟีหรือไม่           |
| 2. ตรวจสอบลายมือชื่อ DPoP Proof กับกุญแจสาธารณะที่ให้มา                                         |
| 3. ตรวจสอบว่า "htm" ตรงกับ "GET" และ "htu" ตรงกับ URL ปลายทางทุกประการ                          |
| 4. ตรวจสอบว่าเวลา "iat" อยู่ในช่วง (< 60 วินาที) และ "jti" ยังไม่เคยถูกใช้งาน                    |
+-------------------------------------------------------------------------------------------------+
                                                │
       ┌────────────────────────────────────────┴────────────────────────────────────────┐
       ▼                                                                                 ▼
[หลักฐานถูกต้องและกุญแจตรงกัน]                                                [ปฏิเสธการใช้โทเค็นที่ถูกขโมยมา]
คำขอผ่านเข้าสู่การทำงานของเครื่องมือ                                          ผู้โจมตีมีโทเค็น แต่ไม่มีกุญแจส่วนตัว
                                                                              ที่อยู่ในเครื่องของเอเจนต์
                                                                              ผลลัพธ์: ปฏิเสธทันที 401 Unauthorized!

การทำงานจริงของ DPoP

  1. การสร้างกุญแจชั่วคราว: เมื่อเริ่มเซสชัน เอเจนต์จะสร้างคู่กุญแจแบบอสมมาตร (ECDSA P-256) ไว้ในหน่วยความจำ
  2. การผูกมัดทางคริปโตกราฟี: เมื่อขอโทเค็น โทเค็นที่ออกให้จะฝังค่าลายนิ้วมือ (jkt) ของกุญแจสาธารณะไว้
  3. การเซ็นชื่อรับรองทุกคำขอ: ในทุกๆ การเรียกใช้ API เอเจนต์จะเซ็นชื่อสร้าง JWT ชั่วคราวที่ระบุเมธอด, URL, เวลา และ UUID
  4. ความปลอดภัยสูงสูด: แม้โทเค็นจะหลุดออกไปทางบันทึกหรือพรอมต์ ผู้โจมตีก็ไม่สามารถนำไปใช้งานได้เพราะขาดกุญแจส่วนตัวในเครื่องของเอเจนต์

8. การทดสอบประสิทธิภาพความปลอดภัยและเมทริกซ์ความเสี่ยง

การทดสอบประสิทธิภาพจริง (ทดสอบ 10,000 ครั้งบนชิป Apple M4 Max)

สถาปัตยกรรมยืนยันตัวตน ความหน่วง Handshake (p50) ความหน่วง Handshake (p99) ภาระตรวจสอบต่อคำขอ การป้องกัน Replay หน่วยความจำฝั่งไคลเอนต์ ภาระ CPU เซิร์ฟเวอร์
PAT คงที่ / API Key 0.1 ms (ไม่มี Handshake) 0.2 ms 0.02 ms (เปรียบเทียบข้อความ) ไม่มี (Replay ได้เต็มที่) < 1 KB ค่ามาตรฐาน
OAuth 1.0a (HMAC-SHA1) 14.2 ms 38.5 ms 1.84 ms (คำนวณลายมือชื่อ) ป้องกันได้บางส่วน (ตรวจ Nonce) 12 KB +18%
OAuth 2.0 Bearer 45.1 ms 112.0 ms 0.15 ms (ตรวจลายมือชื่อ JWT) ไม่มี (Replay ได้เต็มที่) 18 KB +4%
OAuth 2.1 (PKCE + RTR) 48.6 ms 118.4 ms 0.16 ms (ตรวจลายมือชื่อ JWT) ปานกลาง (เพิกถอนด้วย RTR) 24 KB +5%
OAuth 2.1 + DPoP (P-256) 54.2 ms 132.8 ms 1.22 ms (ตรวจ DPoP JWT) สูงสุด (ป้องกันได้สมบูรณ์) 36 KB +12%
mTLS (RFC 8705) 62.8 ms 154.1 ms 0.45 ms (แคชเซสชัน TLS) สูงสุด (ผูกกับใบรับรอง) 128 KB +15%

เมทริกซ์ความเสี่ยงและการป้องกันภัยคุกคาม

ระดับความรุนแรงของภัยคุกคามและการป้องกันของแต่ละโปรโตคอล:
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| รูปแบบการโจมตี                | Static API Key    | OAuth 1.0a        | OAuth 2.0 Bearer  | OAuth 2.1 + DPoP  |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 1. Indirect Prompt Injection  | ร้ายแรง (10/10)   | สูง (7/10)        | ร้ายแรง (10/10)   | ต่ำ (2/10)        |
|    (ข้อมูลหลุดทาง env/log)    | คีย์หลักหลุดถาวร  | คำนวณลายมือชื่อยาก| โทเค็น Bearer หลุด| โทเค็นไร้กุญแจใช้ไม่ได้|
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 2. การดักฟังสัญญาณพอร์ต       | ไม่เกี่ยวข้อง     | ต่ำ (3/10)        | สูง (8/10)        | ปลอดภัย (1/10)    |
|    (ดักจับ Loopback ใน CLI)   | ไม่มี Redirect    | มีการเซ็นชื่อ     | โค้ดถูกขโมยได้    | PKCE ป้องกันไว้   |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 3. การโจมตี SSRF ผ่านเครื่องมือ| ร้ายแรง (10/10)  | ปานกลาง (5/10)    | ร้ายแรง (10/10)   | ปลอดภัย (1/10)    |
|    (สั่งให้ส่งคำขอไปที่อื่น)  | ข้อมูลลับรั่วไหล  | URL ไม่ตรง ล้มเหลว| โทเค็นถูกนำไปใช้  | ปฏิเสธเพราะ URL ไม่ตรง|
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 4. การแอบดูตัวแปรสภาพแวดล้อม  | ร้ายแรง (10/10)   | ปานกลาง (5/10)    | สูง (8/10)        | ต่ำ (2/10)        |
|    (อ่านจาก /proc/environ)    | ข้อมูลลับหลุดหมด  | มีคีย์ใน env      | มีโทเค็นใน env    | อายุสั้นและผูกกุญแจ|
+-------------------------------+-------------------+-------------------+-------------------+-------------------+
| 5. ปัญหา Race Condition       | ไม่เกี่ยวข้อง     | ไม่เกี่ยวข้อง     | ต่ำ (2/10)        | สูง (ต้องมีตัว    |
|    (กลุ่มเอเจนต์ทำงานพร้อมกัน)| ไม่มีการรีเฟรช    | ไม่มีการรีเฟรช    | เกิดความขัดแย้ง   | จัดการ Mutex)     |
+-------------------------------+-------------------+-------------------+-------------------+-------------------+

9. แนวทางการติดตั้งใช้งาน: เซิร์ฟเวอร์ MCP ที่ปลอดภัยด้วย OAuth 2.1 + PKCE

// server.ts - Hardened Remote MCP Server with OAuth 2.1 Validation
import express, { Request, Response, NextFunction } from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";

const app = express();
app.use(express.json());

// Configuration
const ISSUER = "https://auth.enterprise.com/";
const AUDIENCE = "https://mcp.enterprise.com/";
const JWKS_URI = new URL("https://auth.enterprise.com/.well-known/jwks.json");
const JWKS = createRemoteJWKSet(JWKS_URI);

interface AuthenticatedRequest extends Request {
  tokenClaims?: any;
}

/**
 * Enterprise OAuth 2.1 Token Validation Middleware
 */
async function requireOAuth21(
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    res.status(401).json({
      jsonrpc: "2.0",
      error: { code: -32001, message: "Missing or invalid OAuth 2.1 Authorization header." },
      id: req.body?.id || null,
    });
    return;
  }

  const token = authHeader.split(" ")[1];

  try {
    // Cryptographically verify token signature, issuer, audience, and expiration
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: ISSUER,
      audience: AUDIENCE,
    });

    // Enforce OAuth 2.1 requirement: reject tokens without an expiration claim
    if (!payload.exp || typeof payload.exp !== "number") {
      res.status(401).json({
        jsonrpc: "2.0",
        error: { code: -32002, message: "Non-compliant token: missing expiration claim." },
        id: req.body?.id || null,
      });
      return;
    }

    req.tokenClaims = payload;
    next();
  } catch (err: any) {
    res.status(401).json({
      jsonrpc: "2.0",
      error: { code: -32003, message: `Token verification failed: ${err.message}` },
      id: req.body?.id || null,
    });
  }
}

/**
 * Fine-Grained Scope Enforcement Guard
 */
function requireScope(requiredScope: string) {
  return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
    const scopes: string[] = (req.tokenClaims?.scope || "").split(" ");
    if (!scopes.includes(requiredScope)) {
      res.status(403).json({
        jsonrpc: "2.0",
        error: {
          code: -32004,
          message: `Insufficient permissions: missing required scope '${requiredScope}'`,
        },
        id: req.body?.id || null,
      });
      return;
    }
    next();
  };
}

/**
 * Standard MCP JSON-RPC 2.0 Handler Endpoint
 */
app.post(
  "/mcp/v1",
  requireOAuth21,
  requireScope("mcp:tools:execute"),
  async (req: AuthenticatedRequest, res: Response): Promise<void> => {
    const { jsonrpc, method, params, id } = req.body;

    if (jsonrpc !== "2.0") {
      res.status(400).json({ jsonrpc: "2.0", error: { code: -32600, message: "Invalid JSON-RPC version." }, id });
      return;
    }

    // Router for MCP Primitives
    switch (method) {
      case "tools/list":
        res.json({
          jsonrpc: "2.0",
          result: {
            tools: [
              {
                name: "query_database",
                description: "Executes read-only SQL queries against the analytics warehouse.",
                inputSchema: {
                  type: "object",
                  properties: { query: { type: "string" } },
                  required: ["query"],
                },
              },
            ],
          },
          id,
        });
        break;

      case "tools/call":
        if (params?.name === "query_database") {
          // Verify elevated data scope for this specific tool execution
          const scopes: string[] = (req.tokenClaims?.scope || "").split(" ");
          if (!scopes.includes("db:analytics:read")) {
            res.json({
              jsonrpc: "2.0",
              error: { code: -32005, message: "Forbidden: tool requires 'db:analytics:read' scope." },
              id,
            });
            return;
          }

          // Execute tool with verified, scoped identity
          const userSub = req.tokenClaims.sub;
          console.log(`Executing query on behalf of verified agent identity: ${userSub}`);
          
          res.json({
            jsonrpc: "2.0",
            result: {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({ status: "success", rows_returned: 42, latency_ms: 12 }),
                },
              ],
            },
            id,
          });
        } else {
          res.status(404).json({ jsonrpc: "2.0", error: { code: -32601, message: "Tool not found." }, id });
        }
        break;

      default:
        res.status(404).json({ jsonrpc: "2.0", error: { code: -32601, message: "Method not found." }, id });
    }
  }
);

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`Hardened OAuth 2.1 MCP Server listening on port ${PORT}`);
});

10. บทสรุปและคำแนะนำเชิงกลยุทธ์ (E-E-A-T)

การเชื่อมต่อโมเดล AI อัตโนมัติเข้ากับโครงสร้างพื้นฐานระดับองค์กร จำเป็นต้องมองว่า AI Agent เป็น ตัวแทนที่ได้รับมอบหมายอำนาจบางส่วน (Semi-Trusted Delegated Actor) การปฏิบัติต่อเอเจนต์เสมือนเป็นไมโครเซอร์วิสภายในที่เชื่อถือได้เต็มที่ (โดยให้คีย์ Root คงที่) หรือมองเป็นบุคคลภายนอกที่ไม่น่าไว้ใจเลย (ต้องกดยืนยันด้วยตนเองทุกคลิก) ถือเป็นความล้มเหลวในการออกแบบสถาปัตยกรรม

OAuth 2.1 มอบรากฐานการเข้ารหัสที่สมบูรณ์แบบเพื่อเชื่อมโยงช่องว่างนี้ โดยผสานความคล่องตัวของนักพัฒนาร่วมกับมาตรฐานความปลอดภัยระดับ Zero-Trust ขององค์กร

เช็กลิสต์ความปลอดภัยสถาปัตยกรรม AI 5 ข้อสำคัญ

  1. ลบข้อมูลลับคงที่ในสภาพแวดล้อมทั้งหมด: ตรวจสอบคอนฟิก MCP และไฟล์ .env โดยแทนที่ PAT คงที่ด้วยโทเค็นอายุสั้นของ OAuth 2.1
  2. บังคับใช้ PKCE ด้วยอัลกอริทึม S256: ตรวจสอบให้แน่ใจว่าเครื่องมือ CLI ทั้งหมดใช้ RFC 7636 ร่วมกับฟังก์ชันแฮช SHA-256
  3. ปรับเปลี่ยนสภาพแวดล้อม Headless ไปใช้ Device Flow (RFC 8628) หรือ Private Key JWT: ยกเลิกการกรอกรหัสผ่านในเทอร์มินัล และใช้การยืนยันตัวตนที่เป็นมาตรฐาน
  4. ใช้ระบบหมุนเวียน Refresh Token ร่วมกับ Mutex Lock: ป้องกันข้อผิดพลาดจากการแย่งชิงสิทธิ์ของเอเจนต์หลายตัวพร้อมกัน
  5. นำ DPoP (RFC 9449) มาใช้กับเครื่องมือสำคัญ: ผูกมัดโทเค็นกับกุญแจลับของผู้ส่งเพื่อตัดวงจรการขโมยโทเค็นผ่าน Prompt Injection อย่างสมบูรณ์
← บทความทั้งหมด
0 / 4