1. Auth Server là gì trên thực tế và vì sao chúng ta chọn Keycloak
Bắt đầu với một nhắc lại ngắn: Auth Server (IdP) — là dịch vụ mà:
- hiển thị cho người dùng màn hình đăng nhập/đăng ký và consent;
- cấp các token OAuth/OIDC (access_token, id_token, refresh_token);
- công bố discovery document và khóa JWKS để resource‑server có thể kiểm tra các token đó.
Trong stack của chúng ta:
- ChatGPT / MCP Jam đóng vai trò OAuth‑client (public client);
- MCP‑server của bạn — như một Resource Server;
- Keycloak — như một Auth Server.
Vì sao Keycloak tiện cho khóa học và cả đời thực:
- mã nguồn mở, dễ chạy cục bộ/qua Docker;
- mô hình thực thể khá minh bạch: realm, clients, users, roles;
- về cơ bản, mọi cấu hình bạn học trong Keycloak gần như có thể mang sang Auth0/Okta/Cognito 1:1: cùng các ý tưởng — client, scopes, redirect URIs, PKCE.
Ý tưởng quan trọng: chúng ta cấu hình không phải “Keycloak cho toàn bộ dự án”, mà là một Realm cụ thể cho ChatGPT App của chúng ta. Đây là một kiểu “sandbox” xác thực dành riêng cho các MCP‑client.
Tóm lại, cuối bài giảng bạn sẽ có:
- một realm riêng trong Keycloak cho ChatGPT‑app;
- public‑client đã cấu hình Authorization Code + PKCE;
- bộ scopes và claims tối thiểu;
- hiểu cách token “sống” trong Node‑MCP‑server của bạn.
2. Các thực thể cơ bản của Keycloak dưới lăng kính MCP
Để không lúng túng trong trang quản trị, hãy sắp xếp lại các thực thể.
Realm: không gian cấu hình và người dùng
Realm trong Keycloak là một không gian tách biệt với người dùng, client, policy riêng. Một phép so sánh hữu ích — “văn phòng đi thuê trong tòa nhà”: mỗi bên có phòng riêng, danh sách nhân sự riêng và quy tắc vào cửa riêng.
Cho khóa học và App thực tế đầu tiên của bạn, hợp lý là tạo một realm riêng, ví dụ giftgenius-mcp hoặc mcp-course. Điều này cho phép:
- không đụng tới realm master để tránh vô tình làm hỏng trang quản trị;
- tái sử dụng cấu hình và người dùng giữa các môi trường (dev / staging / prod) thông qua xuất/nhập realm.
Client: bản ghi về ứng dụng (ChatGPT / MCP Jam)
Client trong Keycloak không phải “người dùng” mà là ứng dụng đi tới Auth Server để nhận token. Trong trường hợp của chúng ta, đó không phải là backend Next.js của bạn, mà chính là MCP‑client: ChatGPT, MCP Jam, có thể thêm widget riêng nếu bạn thực hiện OAuth thủ công trong UI.
Các trường chính của client:
- client_id — định danh dạng chuỗi;
- loại (public / confidential / bearer-only);
- các luồng OAuth bật (Standard Flow, Client Credentials, v.v.);
- danh sách redirect URI được phép;
- danh sách scopes và protocol mappers (các claim trong token).
Đối với ChatGPT/MCP Jam chúng ta cần public client, vì:
- ChatGPT với vai trò client không thể lưu trữ an toàn client_secret;
- MCP Jam là công cụ desktop/trình duyệt cũng chạy trong môi trường không tin cậy.
User: con người thật
User là người dùng “thật”: có username, mật khẩu, email, thuộc tính, nhóm, vai trò. Khi ai đó đăng nhập qua Keycloak, chính sub và dữ liệu khác của họ sẽ vào token, mà bạn kiểm tra trên MCP‑server và ánh xạ sang accountId / tenantId của bạn.
Ở mức demo của chúng ta là đủ với:
- một hai người dùng thử (ví dụ alice@example.com, bob@example.com);
- có thể — vài thuộc tính như tenant hoặc plan, nếu muốn minh họa cách các claim trong token ảnh hưởng đến hành vi của tools.
3. Chọn loại client: public, PKCE và vì sao không có secret
Bây giờ vào phần cốt lõi: cấu hình client trong Keycloak cho ChatGPT/MCP như thế nào.
Public vs Confidential: vì sao không dùng client_secret
Trong ứng dụng web cổ điển, bạn có backend, đặt client_secret ở đó, và máy chủ đi tới IdP để lấy token. Đó là confidential client: nó có thể lưu trữ secret.
Trong thế giới ChatGPT thì ngược lại:
- OAuth‑client chính là nền tảng ChatGPT hoặc tiện ích như MCP Jam;
- bạn không kiểm soát mã nguồn và môi trường của nó;
- bất kỳ client_secret nào bạn đưa cho ChatGPT đều phải coi như bị lộ ngay lập tức.
Vì vậy ChatGPT/Jam hoạt động như public clients, tức là không có client_secret, và bù lại bằng PKCE — Proof Key for Code Exchange.
PKCE làm gì (nói theo cách “đời thường”)
PKCE là “secret dùng một lần cho phiên”. Mục tiêu của nó là không thể chỉ đơn giản chặn authorization code và đổi nó lấy token từ nơi khác. Sơ đồ như sau:
- Client tạo một chuỗi ngẫu nhiên code_verifier.
- Băm chuỗi đó (thường là SHA-256) để nhận code_challenge.
- Khi redirect tới /authorize, client gửi kèm code_challenge và code_challenge_method=S256.
- Sau khi đăng nhập, người dùng được chuyển về với code trên redirect URI.
- Client gọi POST /token, gửi code và code_verifier gốc.
- Keycloak băm verifier, so sánh với challenge, và nếu khớp thì cấp token.
Điều quan trọng với chúng ta: ChatGPT/MCP Jam làm giúp tất cả bước này. Chúng ta chỉ cần bật hỗ trợ Authorization Code + PKCE (S256) trên client của Keycloak và không yêu cầu client_secret.
4. Cấu hình Keycloak theo từng bước cho kịch bản MCP
Vậy là chúng ta đã xác định: cho ChatGPT/MCP Jam cần public client với Authorization Code Flow và PKCE (S256), không có client_secret. Giờ hãy xem cấu hình này trông như thế nào trong Keycloak.
Giả sử bạn đã có Keycloak chạy (Docker‑container, cài cục bộ — không quan trọng). Lúc này chúng ta quan tâm tới logic cấu hình, không phải “bấm ở đâu”.
Tạo realm mới cho App
Tạo realm giftgenius-mcp: đây là vùng riêng, nơi sẽ có:
- người dùng dành riêng cho ChatGPT‑app;
- các client qua đó ChatGPT/MCP Jam sẽ thực hiện OAuth;
- policy mật khẩu và token riêng.
Mẹo thực tế: đừng trộn realm dùng để ủy quyền nhân viên trang quản trị với realm dành cho client của ChatGPT. Như vậy vừa an toàn hơn, vừa đơn giản hơn về mặt logic.
Thêm người dùng thử nghiệm
Tạo một người dùng, ví dụ alice:
- username: alice;
- email: alice@example.com;
- đặt mật khẩu (đơn giản hóa — không cần policy phức tạp);
- nếu muốn, thêm thuộc tính tenantId=demo-tenant hoặc vai trò ROLE_PREMIUM.
Sau đó, trong MCP‑server, bạn có thể giải mã token, lấy sub, email, tenantId và liên kết chúng với mô hình người dùng của bạn.
Tạo public client cho MCP Jam / ChatGPT
Giờ tới phần thú vị nhất — Client.
Ở mức khái niệm, các tham số nên như sau:
- Client ID: giftgenius-mcp-client (tên tùy ý bạn);
- Loại: public / Client Authentication off;
- bật Standard Flow (Authorization Code);
- bật PKCE với phương thức S256;
- cấu hình redirect URI;
- cấu hình các scope cần thiết (openid + scope tùy biến của bạn, ví dụ mcp:tools).
Bật Standard Flow và PKCE
Về mặt khái niệm:
- bật Authorization Code Flow (thường ghi là “Standard Flow Enabled”);
- trong phần PKCE đặt pkceRequired=true và thường chỉ rõ code_challenge_method=S256.
Vì sao S256: trong tài liệu OAuth 2.1 hiện đại và khuyến nghị của OpenAI/Model Context Protocol, S256 được hỗ trợ như phương pháp an toàn; plain‑PKCE bị coi là không an toàn.
Redirect URI — phần dễ “gãy” nhất
Redirect URI phải khớp từng ký tự với giá trị client sẽ dùng. Nếu không, bạn sẽ gặp lỗi invalid_redirect_uri ở bước cấp quyền.
Trong khóa học, có hai kiểu client điển hình:
- MCP Jam/Inspector để gỡ lỗi. Chúng thường chạy trên http://localhost:PORT/.... Với kịch bản cục bộ, hợp lý là cho phép redirect dạng:
- http://localhost:5173/* hoặc một đường dẫn cụ thể mà Jam sử dụng.
- ChatGPT / Apps SDK ở môi trường production. Ở đây redirect URI do chính nền tảng quy định. Trong tích hợp thực tế, bạn sẽ xem tài liệu cập nhật của OpenAI và cấu hình URL cần thiết mà ChatGPT dùng làm callback.
Trong phạm vi bài giảng, điều quan trọng là hiểu: ChatGPT không thể tùy tiện chọn redirect nào cũng được, nó bắt buộc phải trùng với giá trị đã ghi trên Auth Server. Vì vậy:
- không bao giờ đặt * theo kiểu “URL nào cũng được”;
- đối với phát triển cục bộ có thể chấp nhận wildcard trong phạm vi localhost, nhưng không dùng cho production.
Scopes: tối thiểu nhưng đủ
Scopes là danh sách nhỏ các quyền mà client yêu cầu.
Cho kịch bản MCP của chúng ta, thường cần:
- openid — để bật OpenID Connect và nhận id_token có trường sub, đôi khi có email;
- scope tùy biến, ví dụ mcp:tools, biểu thị “được phép truy cập các công cụ MCP”.
Trong Keycloak, có thể làm qua Client Scopes:
- giữ lại openid;
- tắt mặc định các scope thừa như profile và email nếu bạn không cần;
- thêm scope mới mcp:tools, để sau đó bạn giới hạn quyền gọi tools trên Resource Server.
Điều này quan trọng vì hai lý do:
- Không có openid bạn sẽ không nhận được id_token và một phần trường chuẩn của OIDC.
- Không có scope tùy biến, bạn sẽ không thể nói rõ ràng ở phía MCP‑server: “token này được phép dùng để gọi các công cụ của tôi”.
5. Cấu hình token: thời gian sống, chữ ký và claims
Bây giờ xem Keycloak sẽ cấp những token nào và cấu hình chúng ra sao cho kịch bản MCP.
Thời gian sống của access token
Trong cài đặt realm của Keycloak có phần Tokens, nơi bạn có thể đặt:
- Access Token Lifespan;
- Refresh Token Lifespan và các timeout khác.
Đối với ChatGPT App, access token sống ngắn là quan trọng:
- vài phút hoặc vài giờ — là các giá trị hợp lý;
- nếu token hết hạn, MCP‑server trả 401, ChatGPT chạy lại luồng OAuth, người dùng đăng nhập lại nếu cần.
Ý tưởng giống với tài liệu OpenAI về Apps SDK: TTL ngắn + làm mới token và khả năng “đăng xuất” người dùng khá nhanh bằng cách thu hồi token ở IdP.
Refresh token đối với client ChatGPT, nói chung hoặc là không quá quan trọng, hoặc được cấp với thời hạn nhỏ để tránh giữ phiên vĩnh viễn.
Những claim chúng ta muốn thấy trong token
Tối thiểu cần:
- sub — định danh duy nhất của người dùng trong Keycloak;
- iss — bên cấp token (issuer);
- aud — token dành cho tài nguyên nào (dùng sau ở MCP‑server);
- exp — thời điểm hết hạn;
- scope — danh sách scopes.
Bổ sung thường hữu ích:
- email — nếu bạn muốn thấy địa chỉ người dùng;
- tenantId hoặc claim tương tự — cho kịch bản multi‑tenant;
- roles — cho phân quyền bổ sung.
Trong Keycloak, cấu hình qua Protocol Mappers:
- các mapper chuẩn cho email, preferred_username, v.v.;
- các mapper tùy biến cho thuộc tính người dùng (user.attribute → claim.name).
Ví dụ: mapper thêm email như một claim vào token, chỉ định user.attribute=email, claim.name=email.
Ở phía MCP‑server bạn có thể lấy các claim từ JWT đã parse và:
- liên kết sub với accountId của bạn;
- dùng tenantId để chỉ truy vấn dữ liệu thuộc về tenant đó;
- dùng roles để phân quyền “mịn” hơn.
Chữ ký token và JWKS
Mặc định Keycloak ký access/id token bằng thuật toán bất đối xứng (thường là RS256) và công bố khóa công khai qua JWKS endpoint từ tài liệu OpenID Discovery.
Điều này quan trọng vì MCP‑server có thể:
- lấy issuer từ token;
- theo /.well-known/openid-configuration tìm JWKS endpoint;
- lấy khóa công khai và kiểm tra chữ ký token cục bộ.
Chúng ta sẽ đi sâu phần này trong bài giảng về MCP‑server như một resource server được bảo vệ, nhưng ngay bây giờ cũng hữu ích để hiểu vì sao Keycloak phát hành các siêu dữ liệu này.
6. Dynamic Client Registration (DCR): khi nào thực sự cần
Phần này mang tính nâng cao hơn. Tới giờ chúng ta tạo client “bằng tay” trong trang quản trị — như vậy đã quá đủ để chạy App. Nhưng giao thức OAuth cho phép các client đăng ký động qua một endpoint riêng.
Trong bối cảnh ChatGPT và MCP, OpenAI nói rõ là nền tảng có thể sử dụng Dynamic Client Registration. Tức là ChatGPT tự đăng ký với Auth Server “tại chỗ”, thông qua registration_endpoint từ discovery document.
Ở phía Keycloak, điều này như sau:
- bật DCR trên phạm vi realm;
- bạn cấu hình policy: ai có thể đăng ký client mới và với grant types/scopes nào.
Ví dụ JSON đăng ký public client với Authorization Code + PKCE và scope openid mcp:tools có thể như sau:
{
"clientName": "My ChatGPT App",
"redirectUris": ["https://jam.proxy.mcpapps.com/callback"],
"grantTypes": ["authorization_code"],
"responseTypes": ["code"],
"scope": "openid mcp:tools",
"tokenEndpointAuthMethod": "none"
}
Trong đó tokenEndpointAuthMethod: "none" chính là public client không có client_secret.
Cho khóa học, chỉ cần biết rằng:
- DCR hữu ích khi có nhiều client hoặc chúng sống ngắn;
- ChatGPT có thể tự đăng ký trong IdP của bạn;
- nhưng giai đoạn đầu hoàn toàn có thể dùng client tĩnh tạo qua UI.
7. Điều này liên quan thế nào tới ứng dụng học tập của chúng ta
Nhắc lại: chúng ta có MCP‑server học tập (ví dụ GiftGenius), có thể:
- trả danh sách các món quà phù hợp;
- lưu các danh sách mong muốn của người dùng;
- về sau — gọi phần commerce, tạo đơn hàng, v.v.
Khi MCP‑server để mở, nó không biết ai đang gọi tới:
- yêu cầu từ ChatGPT có thể logic là “của Alice” hoặc “của Bob”, nhưng MCP‑server không phân biệt được;
- bạn không thể hiển thị lịch sử quà tặng riêng tư;
- bạn không thể tự tin trừ tiền đúng tài khoản.
Sau khi cấu hình Keycloak như Auth Server, mọi thứ thay đổi:
- ChatGPT hiểu qua .well-known của tài nguyên MCP của bạn rằng nó được bảo vệ và cần token.
- ChatGPT chuyển người dùng tới Keycloak theo luồng Authorization Code + PKCE.
- Người dùng đăng nhập (người dùng alice của chúng ta).
- ChatGPT nhận access token, trong đó có sub, email, mcp:tools và các claim khác.
- ChatGPT gọi công cụ GiftGenius kèm Authorization: Bearer <token>.
- MCP‑server, khi kiểm tra token, hiểu rằng: “À, đây là Alice với sub=... và tenantId=demo-tenant” — và phản hồi tương ứng.
Mảnh ghép này sẽ được hoàn thiện ở bài giảng tiếp theo, nơi chúng ta biến MCP‑server thành một resource server “đúng nghĩa”: hiện thực endpoint siêu dữ liệu, kiểm tra token và gắn với người dùng.
8. Một vài ví dụ thực hành nhỏ (stack của chúng ta: TypeScript + Node)
Những gì bên dưới không phải “cách duy nhất đúng”, mà là tham chiếu về cách nó có thể trông như trong stack Node/TypeScript thường gặp. Nếu lúc này bạn đang tập trung “bấm” trong Keycloak, có thể lướt nhanh phần này và quay lại khi tích hợp MCP‑server.
Dù cấu hình Keycloak chủ yếu “bấm” trong UI hoặc thông qua Admin REST API của nó, cũng hữu ích khi cho thấy vài đoạn code xung quanh, để hiểu rõ bạn sẽ dùng tất cả thứ này ở phía MCP‑server như thế nào.
Giả sử chúng ta đã có Node.js‑MCP‑server (TypeScript) trên nền SDK chính thức.
Cấu hình ủy quyền (issuer và audience)
Tạo một module nhỏ authConfig.ts:
// authConfig.ts
export const authConfig = {
issuer: 'https://auth.my-company.com/realms/giftgenius-mcp',
audience: 'https://mcp.my-company.com', // URL MCP server của bạn
requiredScopes: ['mcp:tools'], // tối thiểu chúng ta yêu cầu trong token
};
Ở đây issuer — URL của realm Keycloak, audience — định danh tài nguyên (chúng ta sẽ còn dùng nó trong cấu hình token và MCP).
Xác minh JWT cơ bản qua JWKS
Trong thực tế, bạn có thể dùng thư viện như jsonwebtoken + jwks-rsa hoặc tiện ích sẵn có từ MCP SDK. Khung đơn giản có thể như sau:
// verifyToken.ts
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { authConfig } from './authConfig';
const client = jwksClient({
jwksUri: `${authConfig.issuer}/protocol/openid-connect/certs`,
});
function getKey(header: any, callback: any) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key?.getPublicKey();
callback(err, signingKey);
});
}
export function verifyAccessToken(token: string): Promise<any> {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
audience: authConfig.audience,
issuer: authConfig.issuer,
},
(err, decoded) => (err ? reject(err) : resolve(decoded)),
);
});
}
Dĩ nhiên, xử lý lỗi và bộ nhớ đệm khóa nên cẩn thận hơn, nhưng ý tưởng là: Keycloak công bố các khóa JWKS, chúng ta kéo về và kiểm tra chữ ký.
Kiểm tra scope và trích xuất identity
Trong middleware cho MCP‑tools bạn có thể làm như sau:
// authMiddleware.ts
import { verifyAccessToken } from './verifyToken';
import { authConfig } from './authConfig';
export async function requireAuth(bearerToken: string) {
const token = bearerToken.replace(/^Bearer\s+/i, '');
const decoded: any = await verifyAccessToken(token);
const scopes = (decoded.scope as string).split(' ');
const hasScope = authConfig.requiredScopes.every(s => scopes.includes(s));
if (!hasScope) {
throw new Error('Insufficient scope');
}
return {
userId: decoded.sub,
email: decoded.email,
tenantId: decoded.tenantId,
};
}
Và sau đó trong các handler của công cụ MCP, bạn sẽ dùng userId và tenantId để nạp đúng danh sách quà tặng của người dùng. Bản thân các công cụ chúng ta đã hiện thực ở các mô‑đun trước; giờ chỉ cần thấy cách token từ Keycloak trở thành identity mà backend của bạn hiểu được.
9. Các lỗi thường gặp khi cấu hình Keycloak làm MCP Auth Server
Lỗi №1: Dùng confidential client với client_secret.
Đôi khi theo thói quen, người ta tạo client loại confidential và cố gắng điền client_secret vào cấu hình MCP/ChatGPT. Trong hệ sinh thái ChatGPT App, điều này không nên và cũng không an toàn: ChatGPT là public client, nó không thể lưu secret. Cách đúng — public client + PKCE.
Lỗi №2: Scopes mặc định quá rộng.
Giữ bật profile, email và một đống scope chuẩn — rồi phát các token như vậy cho mọi chat — không phải ý hay. Hãy tối thiểu hóa: openid và mcp:tools cụ thể (hoặc vài scope nghiệp vụ) — đủ cho các phiên bản đầu. Điều này giảm rủi ro rò rỉ dữ liệu thừa và làm hành vi dự đoán hơn.
Lỗi №3: Redirect URI không chính xác.
Kinh điển: trong Keycloak ghi http://localhost:5173/callback, còn MCP Jam lại gọi http://localhost:5173/. Hoặc ngược lại. Kết quả — invalid_redirect_uri và quá trình gỡ lỗi cực kỳ bực bội. Luôn kiểm tra giá trị redirect URI chính xác trong tài liệu Jam/ChatGPT và điền khớp từng ký tự.
Lỗi №4: PKCE không bật hoặc bật sai phương thức.
Một số phiên bản Keycloak cần bật riêng “PKCE required” và chỉ định phương thức S256. Nếu không làm vậy, ChatGPT/Jam — vốn kỳ vọng PKCE — có thể nhận lỗi invalid_request với phàn nàn về code_challenge. Hãy luôn kiểm tra cài đặt PKCE cho public‑client.
Lỗi №5: Claims sai hoặc thiếu trong token.
Có trường hợp token không có sub hoặc email vì scope hoặc protocol mapper tương ứng chưa được cấu hình. Kết quả là ở MCP‑server bạn thấy token nhưng không thể ánh xạ nó tới người dùng thực. Giải pháp: bảo đảm các trường cần thiết (tối thiểu sub, tốt hơn có thêm email/tenantId) được map vào access/id token.
Lỗi №6: TTL quá dài cho access token.
Về bảo mật, cấp access token sống một ngày/một tuần — là ý tưởng tệ. Nếu token bị lộ, kẻ tấn công sẽ có quyền truy cập dài hạn vào tài nguyên MCP. Tốt hơn là cấp access token sống ngắn (phút hoặc giờ) và yêu cầu ủy quyền lại khi cần.
Lỗi №7: Nhầm lẫn realm và dùng master.
Đôi khi việc đầu tiên người ta làm là tạo client và người dùng ngay trong realm master. Rồi gắn thêm vài dự án nữa — và cuối cùng chẳng rõ cái nào thuộc về đâu. Hãy tạo realm riêng cho từng ứng dụng/khóa học ngay từ đầu. Điều đó sẽ đơn giản hóa cho cả bạn và DevOps.
GO TO FULL VERSION