Client Registration
ปัญหาที่ OAuth แบบเดิมไม่เคยต้องแก้
หัวข้อที่มีชื่อว่า “ปัญหาที่ OAuth แบบเดิมไม่เคยต้องแก้”OAuth แบบคลาสสิกสมมติว่านักพัฒนา client เข้าไปที่ developer console ของ provider แล้วลงทะเบียนแอปด้วยมือ MCP ทำลายสมมติฐานนั้น: host อเนกประสงค์อย่าง Claude Code หรือ IDE จะเชื่อมต่อกับ server ที่ผู้สร้าง host ไม่เคยได้ยินชื่อ ซึ่งรันโดยองค์กรที่มี authorization server ของตัวเอง ทั้งสองฝ่ายไม่มีทางตกลง client_id กันล่วงหน้าได้
MCP จึงรองรับกลไกลงทะเบียนสามแบบ และบอก client ว่าควรเลือกแบบไหนก่อน:
- Pre-registered credential ถ้า client มีสำหรับ server นี้อยู่แล้ว
- Client ID Metadata Documents (CIMD) ถ้า authorization server ประกาศ
client_id_metadata_document_supported: true - Dynamic Client Registration (DCR) เป็นทางสำรอง ถ้า AS เปิด
registration_endpointไว้ วิธีนี้ deprecated แล้ว - ถาม user ให้วางรายละเอียด client เองถ้าไม่มีทางอื่น
Client ID Metadata Documents
หัวข้อที่มีชื่อว่า “Client ID Metadata Documents”CIMD คือกลไกที่ spec แนะนำ (SHOULD) ตอนนี้ทั้งสำหรับ client และ authorization server ไอเดียคือ identifier ของ client เป็น HTTPS URL และ URL นั้น serve JSON document ที่บรรยาย client:
{ "client_id": "https://app.example.com/oauth/client-metadata.json", "client_name": "Example MCP Client", "client_uri": "https://app.example.com", "logo_uri": "https://app.example.com/logo.png", "redirect_uris": [ "http://127.0.0.1:3000/callback", "http://localhost:3000/callback" ], "grant_types": ["authorization_code"], "response_types": ["code"], "token_endpoint_auth_method": "none"}ข้อกำหนดฝั่ง client:
- URL ของ
client_idMUST ใช้httpsและ MUST มี path (https://example.com/client.jsonไม่ใช่แค่ origin เปล่า ๆ) - document MUST มีอย่างน้อย
client_id,client_nameและredirect_uris - ค่า
client_idใน document MUST ตรงกับ URL ของ document เป๊ะ ๆ - public client (แอป desktop, CLI) ใช้
token_endpoint_auth_method: "none"ส่วน client ที่ถือ key ได้ MAY ใช้private_key_jwtพร้อม JWKS ที่อ้างอิงจาก document
ข้อกำหนดฝั่ง authorization server:
- SHOULD ดึง document เมื่อเจอ
client_idที่เป็น URL - MUST ตรวจว่า
client_idที่ดึงมาเท่ากับ URL, MUST ตรวจว่า document เป็น JSON ที่ถูกต้องและมี field ที่จำเป็นครบ และ MUST ตรวจredirect_uriในทุก authorization request เทียบกับredirect_urisใน document - SHOULD cache document ตาม HTTP cache header
sequenceDiagram participant U as User participant C as MCP Client participant A as Authorization Server participant D as Metadata URL (HTTPS host ของ client) C->>A: authorize (client_id = https://app.example.com/oauth/client.json) A->>U: authenticate A->>D: GET client metadata document D-->>A: JSON (client_id, client_name, redirect_uris) Note over A: ตรวจว่า client_id เท่ากับ URL, redirect_uri อยู่ในรายการ A->>U: หน้า consent แสดง client_name U-->>A: อนุมัติ A-->>C: code ผ่าน redirect_uri
ทำไมดีไซน์นี้ถึงเหมาะกับ MCP:
- ไม่ต้องรู้จักกันมาก่อน AS ไหนก็ตรวจสอบ client ไหนก็ได้ด้วยการดึง document จาก domain ที่ client ควบคุม
- พกพาได้ เพราะ identifier เป็น URL ที่ host เอง
client_idเดียวกันใช้ได้กับทุก authorization server ถ้า server ย้ายไปใช้ AS อื่น client ก็ไม่ต้องลงทะเบียนใหม่ - ตรวจสอบย้อนกลับได้ AS ใช้ trust policy ตาม domain ได้ (allowlist, reputation, ตรวจ certificate) และแสดง domain ให้เห็นชัด ๆ บนหน้า consent
AS ประกาศว่ารองรับใน metadata ของตัวเอง:
{ "client_id_metadata_document_supported": true }client SHOULD ตรวจ flag นี้ และ MAY ถอยไปใช้ DCR หรือ pre-registration เมื่อไม่มี
Pre-registration
หัวข้อที่มีชื่อว่า “Pre-registration”client SHOULD ยังรองรับ credential แบบ static ในรูปแบบใดรูปแบบหนึ่ง:
client_id(และ secret ถ้าเป็น confidential client) ที่ hard-code ไว้สำหรับ authorization server ตัวหนึ่ง แบบเดียวกับที่แอป first-party ทำ- หน้า settings ให้ user วาง
client_idที่ได้จากการลงทะเบียน client ใน console ของ provider ด้วยตัวเอง
Claude Code เปิดแบบที่สองไว้บน command line:
claude mcp add --transport http \ --client-id your-client-id --client-secret --callback-port 8080 \ my-server https://mcp.example.com/mcppre-registered credential ผูกกับ issuer เดียว ถ้า Protected Resource Metadata ของ server เริ่มชี้ไปที่ AS ตัวอื่น client SHOULD แสดง error แทนที่จะเอา credential เก่าไปลองใช้ที่นั่น
Dynamic Client Registration (deprecated)
หัวข้อที่มีชื่อว่า “Dynamic Client Registration (deprecated)”RFC 7591 ให้ client POST metadata ของตัวเองไปที่ registration_endpoint แล้วรับ client_id กลับมาโดยไม่ต้องมี user มาเกี่ยว MCP ยุคแรกพึ่งพามันมาก ตอนนี้มัน deprecated แล้วโดยให้ CIMD มาแทน และคงไว้เฉพาะสำหรับ authorization server ที่ไม่รองรับ metadata document
ถ้าจำเป็นต้องถอยไปใช้ DCR มี field หนึ่งที่บังคับแล้ว:
{ "client_name": "Example MCP Client", "redirect_uris": ["http://127.0.0.1:3000/callback"], "grant_types": ["authorization_code"], "token_endpoint_auth_method": "none", "application_type": "native"}application_type MUST ถูกกำหนด ภายใต้ OpenID Connect Dynamic Client Registration ถ้าไม่ใส่จะ default เป็น "web" แล้ว OIDC server จะปฏิเสธ redirect URI ที่เป็น localhost และ custom scheme ให้ใช้:
"native"สำหรับแอป desktop, แอปมือถือ, CLI และ web app ที่ host ในเครื่องแล้วเข้าผ่านlocalhost"web"สำหรับแอปบน browser ที่ serve จาก host จริง ๆ
server ที่ไม่ใช่ OIDC จะไม่สนใจพารามิเตอร์นี้ ดังนั้นส่งไปเสมอไม่มีข้อเสีย client MUST เตรียมรับมือกับการลงทะเบียนที่ล้มเหลวเพราะข้อจำกัดของ redirect URI, SHOULD แสดง error ที่มีความหมาย และ MAY ลองใหม่ด้วย application_type ที่ปรับแล้วหรือ redirect URI ที่ตรงตามข้อกำหนด
ทำไม DCR ถึงแพ้: AS ทุกตัวจะลงเอยด้วยตารางการลงทะเบียนนิรนามที่โตไม่จำกัด client แต่ละตัวได้ client_id คนละค่า ต่อหนึ่ง AS และการผสมกันระหว่าง client ID แบบ static ฝั่ง upstream กับการลงทะเบียนแบบ dynamic ฝั่ง downstream คือสิ่งที่เปิดทางให้ confused-deputy attack ตามที่อธิบายใน Threats and Mitigations
credential ผูกกับ issuer ของมัน
หัวข้อที่มีชื่อว่า “credential ผูกกับ issuer ของมัน”ไม่ว่า client จะได้ credential มาด้วยวิธีไหน มันเป็นของ authorization server ที่ออกให้:
- client MUST เก็บ credential ที่ persist ไว้ (client ID, secret, token) โดยใช้
issueridentifier ของ AS เป็น key - client MUST NOT เอา credential ไปใช้ซ้ำกับ authorization server ตัวอื่น
- เมื่อ Protected Resource Metadata ของ server ระบุ AS ตัวใหม่ client MUST ลงทะเบียนใหม่ที่นั่น (หรือถ้าใช้ CIMD ก็แค่ยื่น URL เดิม ซึ่ง AS ใหม่จะ resolve เองตอนใช้)
ผลในทางปฏิบัติ: token store ต้องเป็น map จาก issuer ไปยัง credential ไม่ใช่ “token สำหรับ server นี้” แบบแบน ๆ
localhost redirect URI กับความน่าเชื่อถือ
หัวข้อที่มีชื่อว่า “localhost redirect URI กับความน่าเชื่อถือ”native client redirect ไปที่ http://127.0.0.1:PORT/callback CIMD พิสูจน์ได้ว่าผู้สร้าง client ควบคุม app.example.com แต่พิสูจน์ไม่ได้ว่า process ไหนในเครื่องกำลัง listen ที่ port นั้น ผู้โจมตีจึงทำแบบนี้ได้:
- ใช้ metadata URL ของ client ตัวจริงเป็น
client_idของตัวเอง - bind port localhost ไหนก็ได้แล้วส่งเป็น
redirect_uri(มันอยู่ในรายการที่อนุญาต) - รับ authorization code เมื่อ user อนุมัติสิ่งที่ดูเหมือน client ตัวจริง
มาตรการรับมือของ spec อยู่ที่ authorization server: แสดงคำเตือนเพิ่มเมื่อ metadata ของ client มีแต่ redirect URI แบบ localhost, แสดง hostname ของ redirect ให้ชัดตอน consent และใช้ trust policy กับ client ID ที่เป็น URL server ที่ป้องกันเข้มอาจ allowlist domain ส่วน server แบบเปิดอาจรับ HTTPS client_id ไหนก็ได้แต่ยังตรวจ reputation, อายุของ domain หรือ certificate และต้องแสดง hostname ของ client เสมอเพื่อให้ phishing เด่นออกมา
เลือกอย่างไร ในฐานะคนเขียน client
หัวข้อที่มีชื่อว่า “เลือกอย่างไร ในฐานะคนเขียน client”| สถานการณ์ | ใช้ |
|---|---|
| คุณ ship client แบบ first-party สำหรับ AS ของตัวเอง | client_id แบบ pre-registered |
| host อเนกประสงค์ที่เชื่อมต่อ server ไหนก็ได้ | CIMD ที่ host บน domain ของผลิตภัณฑ์คุณ |
AS เก่าและมีแค่ registration_endpoint | DCR พร้อมกำหนด application_type และ log ไว้ว่าถอยมาใช้วิธีนี้ |
| ไม่มีอะไรใช้ได้เลย | ถาม user ให้กรอกรายละเอียด client |