Chỉ có OAuth, nhân viên nào đăng nhập cũng bảo được agent hoàn tiền
Bài thực hành với FastMCP thêm phân quyền theo từng tool: nhân viên chỉ tra cứu, trưởng ca được hoàn tiền, token của hệ thống khác bị từ chối.
Tóm tắt nhanh
- MCP server là resource server: nó tự kiểm tra từng request và audience của token, còn việc cấp token là của authorization server.
- Scope ghi trong token chưa đủ: server cần logic phân quyền riêng, lọc danh sách tool và kiểm tra lại lúc gọi.
- Ở tầng HTTP, token hỏng thì trả 401, token hợp lệ nhưng thiếu quyền thì trả 403. Ở tầng tool, FastMCP ẩn tool và trả not-found.
Một nhân viên chăm sóc khách hàng gõ vào agent: “hoàn tiền vé 4512 giúp tôi”. Agent gọi tool hoan_tien trên MCP server, và giao dịch được thực hiện. Không có ai tấn công cả. Người này đăng nhập hợp lệ, chỉ là server chưa bao giờ hỏi họ có được phép hoàn tiền hay không.
Tài liệu của OpenFGA nói thẳng về rủi ro này: khi không có lớp phân quyền, bất kỳ người dùng nào đã xác thực cũng gọi được bất kỳ tool nào.
Với một FDE, rất có thể đây là điều đội bảo mật của khách hàng muốn được trả lời trước khi cho agent chạm vào dữ liệu thật. Bài này đi qua từng bước để bạn trả lời bằng code chạy được, thay vì bằng slide.
Bạn sẽ dựng gì, và cần chuẩn bị gì?
Ví dụ dùng xuyên suốt là một MCP server giả định cho helpdesk, có hai tool: tra_cuu_ve (chỉ đọc) và hoan_tien (có ghi). Có ba người dùng. An là nhân viên, chỉ được tra cứu. Bình là trưởng ca, được làm cả hai việc. Chi làm ở đội khác và chỉ có token cấp cho hệ thống CRM, không phải cho server này.
Bạn cần Python, thư viện FastMCP, một authorization server hỗ trợ OAuth 2.1 (identity provider của khách hàng, hoặc một bản chạy local để thử) và curl. Các đoạn code bên dưới đã được giản lược để minh họa ý tưởng. Tên import và thuộc tính cụ thể, bạn đối chiếu lại với tài liệu FastMCP trước khi dùng.
Bước 1: chia vai cho rõ, server không tự cấp token
Spec MCP bản 2025-06-18 định nghĩa MCP server được bảo vệ là một OAuth 2.1 resource server. Việc cấp token thuộc về authorization server. Server của bạn không lưu mật khẩu và không có màn hình đăng nhập. Việc của nó là nhận token, kiểm tra token, rồi ra quyết định.
Vì thế, spec bắt buộc MCP server triển khai OAuth 2.0 Protected Resource Metadata (RFC 9728). Tài liệu này cho client biết phải xin token ở authorization server nào. Thiếu nó, client không có cách tự tìm ra đường đăng nhập.
Bước 2: trả 401 sao cho client biết phải đi đâu
Khi một request đến mà không kèm token, spec yêu cầu server trả 401 Unauthorized cùng header WWW-Authenticate chỉ ra vị trí của metadata URL. Đây là thứ bạn nên kiểm tra đầu tiên:
curl -i https://mcp.example.test/mcp
Kết quả cần có dòng 401 và một header WWW-Authenticate chứa URL. Sau đó, curl vào chính URL đó và xác nhận nó trả về metadata trỏ tới authorization server của bạn. Nếu request đầu tiên đã nhận 200, server đang mở cửa cho cả thế giới.
Ở phía client, spec yêu cầu dùng PKCE và gửi tham số resource trong luồng OAuth. Sau đó token phải đi kèm mọi request HTTP, kể cả các request thuộc cùng một session logic, và không bao giờ được đặt trong query string.
Bước 3: kiểm tra token, nhất là audience
Chữ ký hợp lệ thôi thì chưa đủ. Spec yêu cầu server kiểm tra token có được cấp riêng cho chính nó hay không, tức audience có phải là server này không (RFC 8707). Token hết hạn hoặc không hợp lệ phải nhận 401.
Đây chính là trường hợp của Chi. Token của Chi do cùng identity provider cấp và có chữ ký hợp lệ, nhưng nó được cấp cho hệ thống CRM. Nếu chỉ kiểm tra chữ ký, server sẽ nhận nhầm. Tài liệu Security Best Practices của MCP cấm rõ điều này: server không được chấp nhận bất kỳ token nào không cấp riêng cho nó.
Quy tắc này cũng áp dụng theo chiều ngược lại. Khi hoan_tien cần gọi API thanh toán phía sau, đừng chuyển nguyên token của người dùng xuống (token passthrough). Hãy dùng một token riêng được cấp cho lời gọi đó.
Còn một cái bẫy nữa. Cùng tài liệu đó yêu cầu server kiểm tra mọi request đến và không dùng session để xác thực. Session ID chỉ nên dùng để giữ ngữ cảnh và nên gắn với user ID lấy từ token, để kẻ lấy được session của một người không thể dùng nó thay cho người đó.
Bước 4: gắn scope cho từng tool
Đến đây server đã biết ai đang gọi. Câu hỏi tiếp theo là họ được gọi gì. FastMCP cho phép gắn điều kiện vào từng tool qua tham số auth. Theo tài liệu FastMCP, kiểm tra theo scope đòi token phải có đủ mọi scope được chỉ định. Có một trong số đó là chưa đủ.
Tài liệu cũng cho biết auth check có thể là bất kỳ hàm nào nhận AuthContext và trả về boolean. Nhờ vậy bạn tự viết được cả quy tắc theo scope lẫn quy tắc theo người dùng:
# Minh họa, đã giản lược: đối chiếu tên thuộc tính với tài liệu FastMCP
from fastmcp import FastMCP
mcp = FastMCP("helpdesk")
def co_scope(*can_co):
def kiem_tra(ctx) -> bool: # ctx: AuthContext
scopes = set(ctx.token.scopes) # giản lược
return all(s in scopes for s in can_co)
return kiem_tra
@mcp.tool(auth=co_scope("tickets:read"))
def tra_cuu_ve(ma_ve: str) -> dict: ...
@mcp.tool(auth=co_scope("refunds:write"))
def hoan_tien(ma_ve: str) -> dict: ...
Chi tiết đáng chú ý là cách FastMCP xử lý tool không đủ quyền: tool bị ẩn khỏi danh sách, còn nếu cố gọi thẳng thì nhận về not-found. Với An, agent không hề biết có hoan_tien. Nó không thể lỡ tay gọi một tool mà nó không nhìn thấy.
Bước 5: scope chưa đủ, hãy thêm quy tắc theo người dùng
Tài liệu bảo mật của MCP xếp vào nhóm lỗi thường gặp việc coi scope ghi trong token là đủ mà không có logic phân quyền phía server. Lý do rất đời thường: token do bên khác cấp và có thể được cấp rộng tay. Chỉ server của bạn mới biết Bình hiện còn là trưởng ca hay không.
OpenFGA mô tả một mẫu phân quyền theo quan hệ: chỉ cho người dùng thấy những tool họ được dùng, rồi kiểm tra lại khi tool được gọi, vì quyền có thể đã thay đổi giữa chừng. Áp vào ví dụ, bạn viết quy tắc mới rồi thay điều kiện cũ của hoan_tien bằng nó:
# Minh họa: la_truong_ca() tra cứu hệ thống phân quyền của bạn
def truong_ca_va_co_scope(ctx) -> bool:
user_id = ctx.token.claims["sub"] # giản lược
return co_scope("refunds:write")(ctx) and la_truong_ca(user_id)
# Thay auth=co_scope("refunds:write") ở Bước 4 bằng quy tắc mới
@mcp.tool(auth=truong_ca_va_co_scope)
def hoan_tien(ma_ve: str) -> dict: ...
Vì FastMCP áp điều kiện auth cả khi lọc danh sách lẫn khi tool bị gọi thẳng, Bình có scope nhưng đã bị gỡ vai trò trưởng ca sẽ không còn dùng được hoan_tien. Mục tiêu là một ma trận mà ô nào cũng có test:
| Người dùng | tra_cuu_ve | hoan_tien |
|---|---|---|
| An (nhân viên, chỉ có scope đọc) | gọi được | ẩn, gọi thẳng nhận not-found |
| Bình (trưởng ca, đủ scope) | gọi được | gọi được |
| Bình bị gỡ vai trò giữa session | gọi được | bị chặn lúc gọi, nhận not-found |
| Chi (đội khác, token cấp cho CRM) | 401 | 401 |
Dòng thứ ba là dòng hay bị bỏ sót nhất, và cũng là lý do OpenFGA nhấn mạnh việc kiểm tra lại lúc gọi.
401, 403 hay not-found: client thực sự thấy gì?
Spec phân biệt rõ: 401 dành cho token thiếu, hết hạn hoặc không hợp lệ, còn 403 dành cho token hợp lệ nhưng scope sai hoặc không đủ quyền. Nếu trả 401 cho trường hợp thiếu quyền, client sẽ bắt người dùng đăng nhập lại mãi mà vấn đề vẫn còn nguyên.
Cần tách hai tầng. 401 và 403 là mã HTTP, áp dụng khi server từ chối ngay ở cửa vào: token của Chi bị 401 ở đây. Ở tầng tool, quyết định thuộc về điều kiện auth như đã thấy ở Bước 4, nên khi viết test, hãy ghi rõ mỗi ô kỳ vọng mã nào và ở tầng nào.
Tài liệu bảo mật của MCP còn khuyến nghị bắt đầu với scope tối thiểu, rồi mở rộng khi cần bằng cách yêu cầu bổ sung scope qua WWW-Authenticate. Trong ví dụ, mọi người khởi đầu chỉ với tickets:read, và refunds:write chỉ được xin khi có người thực sự cần hoàn tiền.
Trước khi bàn giao, hãy rà lại năm lỗi cần tránh từ các bước trên: chỉ kiểm tra chữ ký mà bỏ qua audience, chuyển tiếp token xuống API phía dưới, tin vào session thay vì kiểm tra từng request, tin hoàn toàn vào scope, và chỉ lọc danh sách tool mà không kiểm tra lại lúc gọi.
Ở site khách hàng, kỹ năng này trông như thế nào?
Thường thì identity provider đã có sẵn và không thuộc quyền của bạn. Việc của bạn là ngồi với đội IAM để thống nhất audience, danh sách scope và nơi lưu các quan hệ kiểu “ai là trưởng ca”. Hãy mang ma trận người dùng × tool đến ngay buổi họp đầu tiên, vì đó là thứ cụ thể nhất để đội bảo mật duyệt.
Nếu muốn chứng minh kỹ năng này khi phỏng vấn, hãy đưa lên GitHub một MCP server nhỏ có test cho từng ô của ma trận, và viết vào CV một dòng như: “phân quyền theo tool cho MCP server, kiểm tra audience, tách 401/403 với not-found ở tầng tool, kiểm tra lại quyền lúc gọi”.
Khi đọc JD có các từ OAuth, MCP hay “least privilege”, bạn sẽ có sẵn một ví dụ cụ thể để kể.
Agent càng làm được nhiều việc thì câu hỏi “ai được phép bảo nó làm gì” càng quan trọng. FDE nào trả lời được câu đó bằng một bảng test xanh hết sẽ là người được khách hàng tin giao dữ liệu thật.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!