1. Bài giảng này nói về gì và không có gì
Đây sẽ là một bài giảng rất thú vị, trong đó chúng ta sẽ:
- ghép lại trong đầu bức tranh “tam giác niềm tin” giữa MCP Client, MCP Server và MCP Auth Server — với người dùng, người đứng “trên” tam giác này với vai trò là chủ sở hữu tài nguyên;
- đi qua flow: ai gửi token cho ai, người dùng đăng nhập ở đâu và vì sao MCP‑server không bao giờ nhìn thấy mật khẩu của họ;
- liên kết điều đó với Next.js/MCP backend của chúng ta và cấu hình Keycloak/Auth0 trong tương lai.
Những gì chúng ta không làm hôm nay:
- không bấm các checkbox trong Keycloak và không cấu hình một IdP cụ thể;
- không viết kiểm tra JWT đầy đủ hoặc introspection — đây là chủ đề của các bài giảng tiếp theo (về Auth Server và về MCP Server như một tài nguyên được bảo vệ).
Mục tiêu bây giờ — để bạn có thể lấy một tờ giấy, vẽ các mũi tên giữa ChatGPT, máy chủ của bạn và Auth0/Keycloak và giải thích trôi chảy: đăng nhập ở đâu, token ở đâu, dữ liệu ở đâu.
2. Tam giác niềm tin: MCP Client, MCP Server, MCP Auth Server
Bắt đầu với các vai trò. “Tam giác niềm tin” kỹ thuật được tạo bởi MCP Client, MCP Server và MCP Auth Server; người dùng (User) — là một vai trò riêng, chủ sở hữu tài nguyên, người đứng như thể ở phía trên tam giác này và cấp đồng ý truy cập. Trong ngữ cảnh MCP và Apps SDK, kiến trúc này được chuẩn hóa khá rõ ràng.
User (Resource Owner)
Đó là con người ở phía bên kia màn hình. Họ:
- truy cập ChatGPT;
- viết yêu cầu “hiển thị đơn hàng của tôi / danh sách quà tặng của tôi”;
- đồng ý “liên kết tài khoản” dịch vụ của bạn với ChatGPT.
Điểm chính: chính họ sở hữu các tài nguyên (lịch sử đơn hàng, hồ sơ, danh sách quà tặng), và chính họ cấp phép truy cập vào chúng.
MCP Client
Ở đây đối với chúng ta là:
- ChatGPT với Apps SDK;
- đôi khi — MCP Jam Inspector (khi gỡ lỗi).
MCP Client có thể:
- đọc metadata của MCP‑server của bạn (qua .well-known);
- khởi chạy OAuth‑flow trong trình duyệt của người dùng;
- lưu trữ và đính kèm token vào các lời gọi công cụ MCP.
Quan trọng là nhớ rằng MCP Client là public client. Nó không lưu client_secret của bạn, vì vậy nó nói chuyện với Auth Server như một ứng dụng SPA công khai: Authorization Code + PKCE.
MCP Server (Resource Server)
Đây là backend của bạn, hiện thực MCP:
- thiết lập kết nối với ChatGPT;
- công bố các công cụ (tools), tài nguyên, prompts;
- trên mỗi lời gọi công cụ, xem header Authorization: Bearer <token>;
- kiểm tra token (chữ ký, exp, aud, scope) và nếu mọi thứ ổn thì thực thi business logic.
Điểm mấu chốt: MCP‑server không xử lý đăng nhập. Nó không thấy mật khẩu, không vẽ form đăng nhập, không gửi email “xác nhận email” cho người dùng. Nó chỉ tin cậy các token được ký số từ Auth Server.
MCP Auth Server (Authorization Server / IdP)
Đây là dịch vụ xác thực và ủy quyền riêng biệt: Keycloak, Auth0, Ory Hydra+Kratos, Okta, Cognito, Azure AD, v.v.
Nó chịu trách nhiệm về:
- UI đăng nhập (email/mật khẩu, SSO, 2FA);
- lưu trữ tài khoản người dùng;
- cấp token (access token, refresh token);
- xuất bản metadata OAuth/OIDC (/authorize, /token, jwks_uri, /registration, v.v.).
Đối với MCP, nó phải hỗ trợ OAuth 2.1 cho public clients (PKCE S256, dynamic client registration, v.v.).
Bảng tổng hợp vai trò
| Ai | Làm gì | Không làm gì |
|---|---|---|
| User | Nhập đăng nhập/mật khẩu, cấp đồng ý truy cập dữ liệu | Không giao tiếp trực tiếp với MCP Server |
| MCP Client (ChatGPT/Jam) | Khởi tạo OAuth, lưu token, gọi MCP tools | Không kiểm tra mật khẩu, không xác minh chữ ký token |
| MCP Server | Kiểm tra token, thực thi business logic của tools | Không vẽ form đăng nhập, không lưu trữ mật khẩu |
| MCP Auth Server | Đăng nhập người dùng, cấp token | Không biết về các công cụ MCP của bạn và business logic của chúng |
Nếu trong đầu bạn tất cả trộn thành một “máy chủ lớn làm mọi thứ” — đã đến lúc tách bạch.
3. Flow trông như thế nào: từ “không có token” đến lời gọi công cụ được bảo vệ
Giờ hãy xem luồng thông điệp. Trong đặc tả MCP, quá trình này được gọi là “The Flow”: discovery → redirect → code → token → authorized calls.
Bước 0. Cố gắng gọi công cụ được bảo vệ mà không có token
Người dùng viết: “Hãy hiển thị các ý tưởng quà tặng đã lưu của tôi”.
ChatGPT với vai trò MCP Client quyết định: “để làm điều đó cần gọi công cụ getUserGiftLists trên MCP‑server của chúng ta”. Nó thực hiện lời gọi không kèm token (người dùng vẫn chưa đăng nhập).
MCP‑server của bạn:
- nhìn thấy thiếu hoặc header Authorization không hợp lệ;
- phản hồi 401 Unauthorized và thêm header WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" với liên kết đến metadata của tài nguyên được bảo vệ (resource metadata, nói thêm bên dưới).
Đại khái như sau (logic, không phải HTTP đầy đủ):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource"
ChatGPT thấy header này và hiểu: “à, tài nguyên được bảo vệ bởi OAuth, cần thực hiện OAuth‑flow và liên kết tài khoản”.
Discovery: .well-known/oauth-protected-resource
Tiếp theo MCP Client yêu cầu metadata từ máy chủ của bạn:
GET /.well-known/oauth-protected-resource
Máy chủ trả về tài liệu JSON với định danh tài nguyên và danh sách máy chủ ủy quyền (authorization servers) để xin token.
Ví dụ tối thiểu (chúng ta sẽ cấu hình chi tiết sau, hiện tại quan trọng là ý tưởng):
{
"resource": "https://api.giftgenius.com",
"authorization_servers": [
"https://auth.giftgenius.com"
],
"scopes_supported": ["gifts.read", "gifts.write"]
}
Ở đây:
- resource — ID chuẩn hóa của tài nguyên; sau đó nó phải được dùng làm audience hoặc resource khi cấp token;
- authorization_servers — danh sách các Auth Server nơi ChatGPT có thể xin token;
- scopes_supported — những “quyền” mà MCP‑server của bạn hiểu.
Authorization Request: chuyển hướng sang Auth Server
Nhận được metadata, MCP Client đi đến Auth Server. Nó mở một tab trong trình duyệt:
GET https://auth.giftgenius.com/authorize
?response_type=code
&client_id=chatgpt-giftgenius
&redirect_uri=... (URL quay lại của MCP Client)
&code_challenge=...
&code_challenge_method=S256
&scope=openid gifts.read
&resource=https://api.giftgenius.com
Người dùng:
- thấy màn hình đăng nhập quen thuộc (ví dụ Keycloak hoặc Auth0);
- nhập đăng nhập/mật khẩu, vượt qua 2FA;
- xác nhận rằng ChatGPT có thể đọc danh sách quà tặng của họ (scope gifts.read).
Code → Token: đổi code lấy token với PKCE
Sau khi đăng nhập thành công, Auth Server chuyển hướng người dùng trở lại MCP Client với code. MCP Client:
- gửi POST đến /token;
- truyền code và code_verifier (khớp với code_challenge từ bước trước).
Auth Server kiểm tra PKCE: băm code_verifier, so sánh với code_challenge ban đầu. Nếu mọi thứ ổn và client thực sự là bên đã bắt đầu flow thì:
- cấp access_token sống ngắn (thường là JWT);
- trong đó chỉ ra:
- sub — ID người dùng trong Auth Server;
- aud hoặc resource — MCP‑server của bạn;
- scope — các hành động được phép (gifts.read, openid, v.v.).
Authenticated Request: gọi công cụ MCP với token
Giờ MCP Client sẵn sàng gọi lại công cụ của bạn, nhưng kèm header:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
MCP‑server:
- kiểm tra chữ ký token (qua JWK của Auth Server) hoặc qua introspection;
- kiểm tra thời hạn (exp);
- kiểm tra aud / resource — token thực sự được cấp cho https://api.giftgenius.com;
- xem scope và quyết định có thể gọi getUserGiftLists hay không.
Sau đó nó truy vấn CSDL của bạn bằng một userId nào đó và trả về danh sách quà tặng cá nhân.
Lưu ý rằng cho đến lúc này chúng ta chỉ nói về luồng mạng: cách token được cấp và đến MCP‑server. Tiếp theo, quan trọng là hiểu cách từ sub và các claim khác trong token suy ra userId cụ thể trong CSDL của bạn — đây là lúc identity bridge tham gia.
4. Identity Bridge: cách user của ChatGPT trở thành userId trong CSDL của bạn
Phần thú vị nhất của kiến trúc — “cầu nối danh tính” (identity bridge). Trong đặc tả MCP nhấn mạnh rằng: MCP‑server không biết về người dùng ChatGPT, nó dựa vào dữ liệu trong token từ Auth Server.
Sơ đồ xấp xỉ như sau:
flowchart TD User[User trong ChatGPT] -->|Login/SSO| Auth[Auth Server] Auth -->|JWT: sub, email, tenant| MCP[MCP Server] MCP -->|userId/tenantId| DB[(CSDL của bạn)]
Chi tiết từng bước như sau.
Thứ nhất, Auth Server biết người dùng của riêng nó: có các thực thể user, email, id, có thể có tenant, roles. Khi đăng nhập thành công, nó đưa thông tin này vào token (các claims):
{
"sub": "auth0|abc123",
"email": "user@example.com",
"given_name": "Alice",
"https://giftgenius.com/tenant": "tenant-42",
"scope": "openid gifts.read",
"aud": "https://api.giftgenius.com"
}
Thứ hai, MCP Server khi kiểm tra token sẽ lấy các claim này và quyết định người đó là ai trong hệ thống của nó. Ví dụ:
- nếu sub đã có trong bảng User.authProviderId — lấy userId liên kết;
- nếu chưa — tạo bản ghi nội bộ (on‑the‑fly provisioning) và liên kết.
Một đoạn mã TypeScript điển hình ở phía MCP‑server (đơn giản hóa, không có xác minh chữ ký) có thể như sau:
type TokenClaims = {
sub: string;
email?: string;
scope?: string;
};
async function mapClaimsToUserId(claims: TokenClaims): Promise<string> {
const user = await db.user.findUnique({ where: { authSub: claims.sub } });
if (user) return user.id;
const created = await db.user.create({
data: { authSub: claims.sub, email: claims.email ?? null }
});
return created.id;
}
Thứ ba, theo userId của riêng mình, MCP‑server lấy mọi thứ cần thiết: danh sách quà tặng, lịch sử đơn hàng, cài đặt, gói cước.
Như vậy, Auth Server trở thành “cầu nối” giữa thế giới bên ngoài (ChatGPT, Google, SSO) và thế giới nội bộ của bạn (customer_id trong CSDL đơn hàng).
5. Vì sao cần tách Auth Server và MCP Server
Đôi khi có cám dỗ: “Hãy để MCP‑server của tôi tự hiển thị đăng nhập và tự cấp token”. Về mặt hình thức có thể (bạn có thể nhúng một IdP mini bên trong), nhưng về kiến trúc đó là ý tưởng không hay. Lý do khá thực tế.
Thứ nhất, bảo mật và khả năng mở rộng. Auth Server là cỗ máy nặng: 2FA, social login, chính sách mật khẩu, khóa tài khoản, khôi phục truy cập, audit đăng nhập, có thể cả chứng chỉ tuân thủ. Viết lại điều này trong mỗi microservice (mỗi MCP‑server) — là con đường xuống địa ngục và PCI‑DSS. Dễ dàng hơn nhiều khi ủy thác cho Keycloak/Auth0 và chỉ cần kiểm tra token của họ.
Thứ hai, tính thay thế client. Hôm nay bạn chỉ có ChatGPT. Ngày mai bạn kết nối Claude Desktop, web frontend của riêng bạn trên Next.js, ứng dụng di động. Tất cả đều có thể dùng cùng một Auth Server và cùng một sơ đồ OAuth 2.1, và MCP‑server của bạn chỉ việc tiếp tục kiểm tra token. Bạn không cần viết lại business logic cho mỗi client mới.
Thứ ba, sự sạch sẽ của mã. MCP Server trong lý tưởng:
- biết xuất bản /.well-known/oauth-protected-resource;
- biết kiểm tra Bearer-token và rút ra từ nó userId, scopes, tenant;
- hiện thực các công cụ nghiệp vụ (orders, gifts, profiles).
Toàn bộ UI đăng nhập — form, giao diện, social login — sống trong Auth Server và không làm nặng backend.
6. Trông như thế nào trong ứng dụng học tập GiftGenius
Quay lại ứng dụng mà chúng ta theo xuyên suốt khóa học. Giả sử chúng ta có:
- ChatGPT App “GiftGenius” với widget (Apps SDK), có thể gợi ý quà tặng;
- MCP‑server chạy Node/Next.js, cung cấp các công cụ:
- searchGifts — ẩn danh, không yêu cầu đăng nhập;
- getSavedGiftLists — cá nhân, yêu cầu xác thực;
- Auth Server (sau này — Keycloak/Auth0), nơi mỗi người dùng có một tài khoản.
Kịch bản người dùng ẩn danh và đã đăng nhập
Nếu người dùng chỉ viết “gợi ý quà cho anh trai, 30 tuổi, thích board game”, App của chúng ta có thể:
- gọi công cụ ẩn danh searchGifts;
- trả về khuyến nghị trong giao diện.
Trong trường hợp này:
- không cần token;
- MCP‑server chỉ việc thực thi truy vấn (ví dụ đến catalog của bạn hoặc API bên thứ ba).
Ngay khi người dùng nói “lưu cái này vào danh sách của tôi” hoặc “hiển thị các ý tưởng đã lưu của tôi”, mô hình quyết định gọi công cụ được bảo vệ getSavedGiftLists. Server trả về 401 + WWW-Authenticate với resource_metadata. ChatGPT khởi chạy OAuth‑wizard “Link GiftGenius account”, đưa người dùng qua đăng nhập và lấy token.
Tiếp theo, ở mỗi lời gọi được bảo vệ:
- MCP Server đã thấy Authorization: Bearer ...;
- rút ra userId từ token;
- lọc dữ liệu theo userId này.
Chính nhờ vậy chúng ta có thể:
- tách biệt dữ liệu của các người dùng khác nhau;
- hiển thị an toàn lịch sử đơn hàng, danh sách yêu thích;
- thực hiện các chức năng thương mại (về sau trong khóa).
Kiến trúc backend: middleware + handler công cụ
Trong thực tế với Node/Next.js, điều này thường là chuỗi: “middleware xác thực → business handler của công cụ”. Trong bài giảng về hiện thực tool‑handler chúng ta đã nhấn mạnh rằng cần truyền context vào đó: user_id, token, cài đặt.
Đoạn mã có thể như sau:
// auth-context.ts
export type AuthContext = {
userId: string | null; // null cho lời gọi ẩn danh
scopes: string[];
};
Middleware gắn vào tất cả MCP‑endpoint:
// mcp-auth-middleware.ts
export async function buildAuthContext(req: Request): Promise<AuthContext> {
const header = req.headers.authorization || "";
const token = header.replace(/^Bearer\s+/i, "");
if (!token) return { userId: null, scopes: [] }; // ẩn danh
const claims = await verifyAndDecodeToken(token); // xác minh token
const userId = await mapClaimsToUserId(claims);
const scopes = (claims.scope || "").split(" ");
return { userId, scopes };
}
Và chính handler của công cụ nhận context này:
// tools/getSavedGiftLists.ts
export async function getSavedGiftLists(_args: {}, ctx: AuthContext) {
if (!ctx.userId) throw new Error("User must be authenticated");
return db.giftList.findMany({
where: { ownerId: ctx.userId }
});
}
Ý nghĩa là tool‑handler không biết gì về OAuth hay PKCE. Nó chỉ làm việc với userId “hiển nhiên”. Toàn bộ phép màu OAuth được giấu phía trước: trong MCP‑client và trong Auth‑middleware.
7. Sơ đồ trực quan: Client, Server và Auth sống cùng nhau thế nào
Chúng ta đã đi tuần tự luồng trong mục 3 bằng văn bản. Đôi khi vẽ một lần dễ hơn nói bảy lần, nên bây giờ hiển thị cùng các tương tác đó bằng hai sơ đồ.
Khung tương tác (The Triangle of Trust)
flowchart TD U[User] -->|1. Login / Consent| A[MCP Auth Server] U -->|2. Trò chuyện| C["MCP Client (ChatGPT)"] C -->|3. OAuth Flow| A C -->|4. Bearer Token| S[MCP Server] S -->|5. Data| C
Sơ đồ đọc như sau.
Đầu tiên người dùng đăng nhập qua Auth Server, về bản chất xác nhận danh tính và cấp token. MCP Client điều phối quá trình này và sau đó dùng token để gọi MCP‑server. MCP‑server không thấy đăng nhập/mật khẩu, nó chỉ thấy token và quyết định điều gì được phép.
Luồng từ yêu cầu đến phản hồi
sequenceDiagram participant User participant ChatGPT as MCP Client participant Auth as Auth Server participant MCP as MCP Server User->>ChatGPT: "Hiển thị danh sách quà tặng đã lưu của tôi" ChatGPT->>MCP: callTool(getSavedGiftLists) (không có token) MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata) ChatGPT->>Auth: /authorize + PKCE User->>Auth: Nhập đăng nhập/mật khẩu, cấp consent Auth-->>ChatGPT: redirect + code ChatGPT->>Auth: /token + code_verifier Auth-->>ChatGPT: access_token (JWT) ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ... MCP-->>ChatGPT: JSON với danh sách cá nhân ChatGPT-->>User: Danh sách được hiển thị trong widget
Đây là sơ đồ mà đến cuối module bạn nên có thể “kể lại khi nhắm mắt”.
8. Sâu hơn một chút: nhiều tài nguyên, nhiều client, DCR
Điều hay của kiến trúc này — nó mở rộng rất tốt.
Thứ nhất, bạn có thể có nhiều MCP‑server (ví dụ, một về quà tặng, một về đơn hàng), và một Auth Server, nơi cấp token với các aud/resource khác nhau. Mỗi máy chủ tài nguyên bắt buộc phải kiểm tra rằng token thực sự được cấp cho chính nó, nếu không sẽ xảy ra vấn đề cổ điển “confused deputy”, khi token cho một dịch vụ lại được chấp nhận bởi dịch vụ khác.
Thứ hai, bạn có thể có nhiều client:
- ChatGPT App;
- frontend riêng của bạn;
- ứng dụng di động;
- tích hợp đối tác qua MCP Gateway.
Tất cả sẽ:
- đọc /.well-known/oauth-protected-resource;
- biết Auth Server ở đâu;
- đi qua OAuth 2.1 flow;
- nhận token và gọi MCP‑server.
Thứ ba, các Auth Server hiện đại ngày càng hỗ trợ Dynamic Client Registration (DCR) — khả năng đăng ký client động qua API. Đặc tả MCP cũng hàm ý khả năng này: client (ChatGPT/Jam) có thể tự động đăng ký chính nó trên Auth Server qua registration_endpoint.
Trong module này, điều quan trọng là hiểu rằng:
- MCP Client, MCP Server và Auth Server giao tiếp qua các tài liệu discovery chuẩn hóa và qua token;
- bạn không cần “hardcode” tất cả client trong mã backend;
- bạn có thể mở rộng hệ sinh thái mà không phá vỡ mô hình ủy quyền hiện có.
9. Các lỗi thường gặp khi hiểu kiến trúc ủy quyền MCP
Lỗi số 1: “MCP‑server phải tự đăng nhập người dùng”.
Đôi khi lập trình viên cố nhúng form đăng nhập ngay trong MCP‑server, rồi gửi đăng nhập/mật khẩu qua các công cụ. Điều này phá vỡ chính ý tưởng của OAuth. MCP‑server không được phép thấy mật khẩu trong bất kỳ trường hợp nào. Đăng nhập và consent — thuộc trách nhiệm của Auth Server. MCP‑server chỉ làm việc với token và các claim của nó.
Lỗi số 2: Nhầm lẫn giữa MCP Client và MCP Server.
Có lúc người ta xem ChatGPT như “một phần backend của tôi” và cố, ví dụ, lưu trữ bí mật trong đó hoặc kỳ vọng nó tự kiểm tra quyền truy cập. Thực tế MCP Client chỉ khởi tạo OAuth và đính kèm token. Kiểm tra token và quyền — là nhiệm vụ của MCP‑server, không phải ChatGPT.
Lỗi số 3: “API‑key trong .env thay cho OAuth”.
Anti‑pattern kinh điển: tạo một SERVICE_API_KEY lớn, đặt nó vào .env của MCP‑server và nghĩ rằng đã xong. Cách này không có phân quyền theo người dùng, không thể an toàn hiển thị dữ liệu cá nhân hoặc thực hiện mua hàng, mọi thứ đều làm “nhân danh dịch vụ”, không phải người dùng. Điều này hoàn toàn trái với mục tiêu ủy quyền trong ChatGPT Apps.
Lỗi số 4: Bỏ qua audience và resource.
Nếu MCP‑server chấp nhận bất kỳ JWT hợp lệ nào với chữ ký đúng mà không kiểm tra aud/resource, thì bất kỳ token nào cấp cho dịch vụ khác bởi cùng Auth Server cũng có thể được dùng để gọi công cụ của bạn. Đây là vi phạm trực tiếp mô hình bảo mật của OAuth. Server bắt buộc phải kiểm tra token được cấp cho chính resource của mình.
Lỗi số 5: Trộn lẫn logic auth và business logic.
Đôi khi trong tool‑handler người ta kéo vào toàn bộ việc phân tích token, kiểm tra chữ ký, làm việc với JWK, v.v. Kết quả là mã dễ vỡ và khó bảo trì. Đúng đắn hơn là tách lớp “kiểm tra token, ánh xạ sang userId” (middleware) khỏi lớp “logic của công cụ” vốn nhận AuthContext đã rõ ràng.
Lỗi số 6: Kỳ vọng ChatGPT “tự làm hết” mà không có .well-known.
Không có endpoint /.well-known/oauth-protected-resource đúng, MCP‑client đơn giản là không biết Auth Server của bạn ở đâu và cần scopes gì. Kết quả — chat im lặng “không thể đăng nhập”, còn lập trình viên nhìn vào log trống. Con đường đúng: MCP‑server công bố rõ ràng yêu cầu ủy quyền của mình qua .well-known, client đọc nó và xây flow.
Lỗi số 7: Quên gắn người dùng trong business logic.
Đôi khi, ngay cả khi đã cấu hình đúng OAuth và ánh xạ token sang userId, lập trình viên vẫn không dùng điều đó trong truy vấn CSDL: ví dụ, quên lọc theo ownerId = userId. Khi đó bất kỳ người dùng đã xác thực nào cũng có thể thấy dữ liệu của người khác. Có token chỉ là bước đầu; bước tiếp theo luôn là sử dụng đúng userId và scope trong mã nghiệp vụ.
GO TO FULL VERSION