Skip to content
Lesson 10/11 · MCP cho người mới bắt đầu (Microsoft)

Xác thực đơn giản cho MCP server

Từ xác thực bằng secret trong header Authorization qua middleware, tới JWT và phân quyền theo vai trò (RBAC) cho từng tool của MCP server.

This content is currently available in Vietnamese only.

Intermediate@shine190423 min read

Các MCP SDK hỗ trợ OAuth 2.1, một quy trình khá phức tạp gồm các khái niệm như auth server, resource server, gửi thông tin đăng nhập, nhận code, đổi code lấy bearer token cho tới khi lấy được dữ liệu resource. Nếu bạn chưa quen OAuth (một thứ rất nên cài đặt), nên bắt đầu với mức xác thực cơ bản rồi nâng dần độ bảo mật. Bài này tồn tại vì lý do đó: dẫn bạn từng bước tới xác thực nâng cao.

"Auth" nghĩa là gì?

Auth là viết tắt của authentication và authorization. Ta cần làm hai việc:

  • Xác thực (authentication): quá trình xác định có cho một người vào nhà hay không, tức họ có quyền "ở đây", truy cập resource server nơi đặt các tính năng của MCP server.
  • Phân quyền (authorization): quá trình xác định người dùng có được truy cập các resource cụ thể họ yêu cầu hay không, ví dụ các đơn hàng hay sản phẩm này, hoặc được đọc nội dung nhưng không được xoá.

Thông tin xác thực: cách cho hệ thống biết ta là ai

Phần lớn lập trình viên web nghĩ tới việc gửi một thông tin xác thực (credential) cho server, thường là một secret cho biết họ có được phép ở đây không ("xác thực"). Credential này thường là username và password mã hoá base64, hoặc một API key định danh duy nhất một người dùng.

Nó được gửi qua header tên "Authorization" như sau:

json
{ "Authorization": "secret123" }

Cách này thường gọi là basic authentication. Luồng tổng thể hoạt động như sau:

mermaid
sequenceDiagram
   participant User
   participant Client
   participant Server

   User->>Client: show me data
   Client->>Server: show me data, here's my credential
   Server-->>Client: 1a, I know you, here's your data
   Server-->>Client: 1b, I don't know you, 401 

Tức là: người dùng nhờ client lấy dữ liệu; client gửi yêu cầu kèm credential; nếu server nhận ra credential thì trả dữ liệu (1a), nếu không thì trả lỗi 401 (1b).

Đã hiểu luồng, cài đặt thế nào? Phần lớn web server có khái niệm middleware: một đoạn code chạy trong quá trình xử lý request, có thể kiểm tra credential; nếu hợp lệ thì cho request đi tiếp, nếu không thì trả lỗi xác thực. Xem cách cài đặt:

python
class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):

        has_header = request.headers.get("Authorization")
        if not has_header:
            print("-> Missing Authorization header!")
            return Response(status_code=401, content="Unauthorized")

        if not valid_token(has_header):
            print("-> Invalid token!")
            return Response(status_code=403, content="Forbidden")

        print("Valid token, proceeding...")
       
        response = await call_next(request)
        # add any customer headers or change in the response in some way
        return response


starlette_app.add_middleware(CustomHeaderMiddleware)

Ở đây chúng ta:

  • Tạo middleware tên AuthMiddleware, có phương thức dispatch được web server gọi.

  • Thêm middleware vào web server:

    python
    starlette_app.add_middleware(AuthMiddleware)
    
  • Viết logic kiểm tra header Authorization có mặt và secret gửi lên có hợp lệ không:

    python
    has_header = request.headers.get("Authorization")
    if not has_header:
        print("-> Missing Authorization header!")
        return Response(status_code=401, content="Unauthorized")
    
    if not valid_token(has_header):
        print("-> Invalid token!")
        return Response(status_code=403, content="Forbidden")
    

    Nếu secret có mặt và hợp lệ, ta cho request đi tiếp bằng cách gọi call_next và trả về response:

    python
    response = await call_next(request)
    # add any customer headers or change in the response in some way
    return response
    

Khi có web request tới server, middleware được gọi, và tuỳ cách cài đặt, nó cho request đi qua hoặc trả lỗi cho biết client không được phép tiếp tục.

Bài thực hành: cài đặt xác thực

Hãy áp dụng kiến thức vừa học. Kế hoạch:

Server:

  • Tạo web server và instance MCP.
  • Cài đặt middleware cho server.

Client:

  • Gửi web request kèm credential qua header.

Bước 1: tạo web server và instance MCP

Bước đầu, ta tạo instance web server và MCP server.

Ở đây ta tạo instance MCP server, tạo web app starlette và host bằng uvicorn.

python
# creating MCP Server

app = FastMCP(
    name="MCP Resource Server",
    instructions="Resource Server that validates tokens via Authorization Server introspection",
    host=settings["host"],
    port=settings["port"],
    debug=True
)

# creating starlette web app
starlette_app = app.streamable_http_app()

# serving app via uvicorn
async def run(starlette_app):
    import uvicorn
    config = uvicorn.Config(
            starlette_app,
            host=app.settings.host,
            port=app.settings.port,
            log_level=app.settings.log_level.lower(),
        )
    server = uvicorn.Server(config)
    await server.serve()

run(starlette_app)

Trong code này, chúng ta:

  • Tạo MCP server.
  • Dựng web app starlette từ MCP server bằng app.streamable_http_app().
  • Host và phục vụ web app bằng uvicorn server.serve().

Tiếp theo là tạo middleware để kiểm tra credential gửi tới.

Bước 2: cài đặt middleware cho server

Ta tạo một middleware tìm credential trong header Authorization và kiểm tra nó. Nếu chấp nhận được, request sẽ đi tiếp làm việc cần làm (ví dụ liệt kê tool, đọc resource hay bất kỳ chức năng MCP nào client yêu cầu).

Để tạo middleware, ta tạo một lớp kế thừa BaseHTTPMiddleware. Có hai phần đáng chú ý:

  • Request request, nơi ta đọc thông tin header.
  • call_next, callback cần gọi nếu client mang credential được chấp nhận.

Trước hết, xử lý trường hợp thiếu header Authorization:

python
has_header = request.headers.get("Authorization")

# no header present, fail with 401, otherwise move on.
if not has_header:
    print("-> Missing Authorization header!")
    return Response(status_code=401, content="Unauthorized")

Ở đây ta gửi thông báo 401 unauthorized vì client không vượt qua bước xác thực.

Tiếp theo, nếu có credential, ta kiểm tra tính hợp lệ:

python
 if not valid_token(has_header):
    print("-> Invalid token!")
    return Response(status_code=403, content="Forbidden")

Để ý ta gửi thông báo 403 forbidden ở trên. Đây là middleware đầy đủ cài đặt mọi điều nói trên:

python
class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):

        has_header = request.headers.get("Authorization")
        if not has_header:
            print("-> Missing Authorization header!")
            return Response(status_code=401, content="Unauthorized")

        if not valid_token(has_header):
            print("-> Invalid token!")
            return Response(status_code=403, content="Forbidden")

        print("Valid token, proceeding...")
        print(f"-> Received {request.method} {request.url}")
        response = await call_next(request)
        response.headers['Custom'] = 'Example'
        return response

Còn hàm valid_token thì sao? Đây:

python
# DON'T use for production - improve it !!
def valid_token(token: str) -> bool:
    # remove the "Bearer " prefix
    if token.startswith("Bearer "):
        token = token[7:]
        return token == "secret-token"
    return False

Hàm này rõ ràng cần được cải thiện.

Quan trọng: bạn KHÔNG BAO GIỜ được để secret trong code như thế này. Lý tưởng nhất là lấy giá trị để so sánh từ một nguồn dữ liệu hoặc từ IDP (identity provider, nhà cung cấp định danh), hoặc tốt hơn nữa là để IDP thực hiện việc kiểm tra.

Ta đã thiết lập web server dùng middleware để kiểm tra credential mà client gửi lên. Còn bản thân client thì sao?

Bước 3: gửi web request kèm credential qua header

Ta cần đảm bảo client gửi credential qua header. Vì dùng MCP client để làm việc này, cần biết cách làm với nó.

Ở client, ta truyền header chứa credential như sau:

python
# DON'T hardcode the value, have it at minimum in an environment variable or a more secure storage
token = "secret-token"

async with streamablehttp_client(
        url = f"http://localhost:{port}/mcp",
        headers = {"Authorization": f"Bearer {token}"}
    ) as (
        read_stream,
        write_stream,
        session_callback,
    ):
        async with ClientSession(
            read_stream,
            write_stream
        ) as session:
            await session.initialize()
      
            # TODO, what you want done in the client, e.g list tools, call tools etc.

Để ý cách ta điền thuộc tính headers: headers = {"Authorization": f"Bearer {token}"}.

Quan trọng: vậy cải thiện tiếp thế nào? Cách cài đặt hiện tại có vài vấn đề. Trước hết, gửi credential như thế này khá rủi ro nếu ít nhất không có HTTPS. Ngay cả khi có, credential vẫn có thể bị đánh cắp, nên bạn cần một hệ thống thu hồi token dễ dàng và thêm các kiểm tra khác như request đến từ đâu trên thế giới, request có xảy ra quá thường xuyên không (hành vi giống bot); tóm lại có rất nhiều mối lo.

Dù vậy, với các API rất đơn giản mà bạn không muốn ai gọi khi chưa xác thực, những gì ta có ở đây là khởi đầu tốt.

Giờ hãy tăng cường bảo mật một chút bằng một định dạng chuẩn là JSON Web Token, còn gọi là JWT hay token "JOT".

JSON Web Token (JWT)

Ta muốn cải thiện so với việc gửi credential rất đơn giản. Dùng JWT mang lại những cải thiện nào ngay?

  • Bảo mật tốt hơn. Với basic auth, bạn gửi lặp đi lặp lại username và password dạng base64 (hoặc API key), làm tăng rủi ro. Với JWT, bạn gửi username và password và nhận lại một token có giới hạn thời gian, tức sẽ hết hạn. JWT cũng giúp dễ dàng kiểm soát truy cập chi tiết bằng role, scope và permission.
  • Không trạng thái và mở rộng tốt. JWT tự chứa: mang mọi thông tin người dùng, không cần lưu session phía server. Token cũng kiểm tra được ở local.
  • Khả năng tương tác (interoperability) và liên kết danh tính. JWT là trung tâm của Open ID Connect, được dùng với các nhà cung cấp định danh nổi tiếng như Entra ID, Google Identity và Auth0. Nó cũng cho phép đăng nhập một lần (single sign on) và nhiều thứ khác, đạt chuẩn doanh nghiệp.
  • Mô-đun và linh hoạt. JWT dùng được với API Gateway như Azure API Management, NGINX… Nó cũng hỗ trợ các tình huống xác thực người dùng và giao tiếp server với dịch vụ, gồm cả mạo danh (impersonation) và uỷ quyền (delegation).
  • Hiệu năng và cache. JWT có thể được cache sau khi giải mã, giảm việc phân tích lại. Điều này đặc biệt có ích với ứng dụng nhiều lưu lượng vì tăng thông lượng và giảm tải hạ tầng.
  • Tính năng nâng cao. JWT còn hỗ trợ introspection (kiểm tra tính hợp lệ trên server) và thu hồi (làm token mất hiệu lực).

Với những lợi ích này, hãy nâng phần cài đặt lên tầm mới.

Chuyển từ basic auth sang JWT

Ở mức tổng quan, cần thay đổi:

  • Học cách dựng JWT token và chuẩn bị để gửi từ client tới server.
  • Kiểm tra JWT token, nếu hợp lệ thì cho client truy cập resource.
  • Lưu token an toàn: cách lưu token này.
  • Bảo vệ route: ở đây ta cần bảo vệ các route và các tính năng MCP cụ thể.
  • Thêm refresh token: đảm bảo access token sống ngắn, còn refresh token sống dài để lấy token mới khi token cũ hết hạn. Cũng cần có endpoint refresh và chiến lược xoay vòng (rotation).

Bước 1: dựng JWT token

JWT token gồm các phần:

  • header: thuật toán dùng và loại token.
  • payload: các claim, như sub (người dùng hoặc thực thể mà token đại diện; trong tình huống xác thực thường là user id), exp (thời điểm hết hạn), role (vai trò).
  • signature: chữ ký bằng secret hoặc private key.

Ta cần dựng header, payload và token đã mã hoá.

python

import jwt
import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
import datetime

# Secret key used to sign the JWT
secret_key = 'your-secret-key'

header = {
    "alg": "HS256",
    "typ": "JWT"
}

# the user info and its claims and expiry time
payload = {
    "sub": "1234567890",               # Subject (user ID)
    "name": "User Userson",                # Custom claim
    "admin": True,                     # Custom claim
    "iat": datetime.datetime.utcnow(),# Issued at
    "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)  # Expiry
}

# encode it
encoded_jwt = jwt.encode(payload, secret_key, algorithm="HS256", headers=header)

Trong code trên, chúng ta:

  • Định nghĩa header với thuật toán HS256 và loại JWT.
  • Dựng payload chứa subject hay user id, tên người dùng, vai trò, thời điểm phát hành và thời điểm hết hạn, qua đó cài đặt tính giới hạn thời gian đã nói ở trên.

Bước 2: kiểm tra token

Ta cũng cần kiểm tra token; việc này làm ở server để chắc thứ client gửi lên thực sự hợp lệ. Có nhiều kiểm tra nên làm, từ cấu trúc tới hiệu lực. Bạn cũng nên thêm các kiểm tra khác, như người dùng có tồn tại trong hệ thống không.

Để kiểm tra token, ta giải mã để đọc rồi bắt đầu kiểm tra tính hợp lệ:

python

# Decode and verify the JWT
try:
    decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
    print("✅ Token is valid.")
    print("Decoded claims:")
    for key, value in decoded.items():
        print(f"  {key}: {value}")
except ExpiredSignatureError:
    print("❌ Token has expired.")
except InvalidTokenError as e:
    print(f"❌ Invalid token: {e}")

Trong code này, ta gọi jwt.decode với token, secret key và thuật toán đã chọn. Để ý ta dùng try-catch vì kiểm tra thất bại sẽ phát sinh lỗi.

Lưu ý: như đã nói, ta nên thực hiện thêm kiểm tra để đảm bảo token trỏ tới một người dùng trong hệ thống và người dùng đó có đúng quyền mà token khẳng định.

Tiếp theo, hãy xem kiểm soát truy cập dựa trên vai trò, còn gọi là RBAC.

Thêm kiểm soát truy cập dựa trên vai trò (RBAC)

Ý tưởng là các vai trò khác nhau có quyền khác nhau. Ví dụ, ta giả định admin làm được mọi thứ, người dùng thường đọc/ghi được, còn khách chỉ đọc được. Vì vậy, một số mức quyền có thể là:

  • Admin.Write
  • User.Read
  • Guest.Read

Hãy xem cách cài đặt kiểm soát như vậy bằng middleware. Middleware có thể thêm cho từng route hoặc cho mọi route.

python
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
import jwt

# DON'T have the secret in the code like, this is for demonstration purposes only. Read it from a safe place.
SECRET_KEY = "your-secret-key" # put this in env variable
REQUIRED_PERMISSION = "User.Read"

class JWTPermissionMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        auth_header = request.headers.get("Authorization")
        if not auth_header or not auth_header.startswith("Bearer "):
            return JSONResponse({"error": "Missing or invalid Authorization header"}, status_code=401)

        token = auth_header.split(" ")[1]
        try:
            decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        except jwt.ExpiredSignatureError:
            return JSONResponse({"error": "Token expired"}, status_code=401)
        except jwt.InvalidTokenError:
            return JSONResponse({"error": "Invalid token"}, status_code=401)

        permissions = decoded.get("permissions", [])
        if REQUIRED_PERMISSION not in permissions:
            return JSONResponse({"error": "Permission denied"}, status_code=403)

        request.state.user = decoded
        return await call_next(request)


Có vài cách thêm middleware như dưới đây:

python

# Alt 1: add middleware while constructing starlette app
middleware = [
    Middleware(JWTPermissionMiddleware)
]

app = Starlette(routes=routes, middleware=middleware)

# Alt 2: add middleware after starlette app is already constructed
starlette_app.add_middleware(JWTPermissionMiddleware)

# Alt 3: add middleware per route
routes = [
    Route(
        "/mcp",
        endpoint=..., # handler
        middleware=[Middleware(JWTPermissionMiddleware)]
    )
]

Bạn đã thấy middleware dùng được cho cả xác thực lẫn phân quyền. Còn MCP thì sao, nó có thay đổi cách làm auth không? Hãy xem phần tiếp theo.

Bước 3: thêm RBAC vào MCP

Bạn đã thấy cách thêm RBAC qua middleware. Tuy nhiên, với MCP không có cách dễ dàng để thêm RBAC cho từng tính năng MCP, vậy làm gì? Ta phải thêm code kiểm tra, ở đây là kiểm tra client có quyền gọi một tool cụ thể không. Có vài lựa chọn:

  • Thêm kiểm tra cho từng tool, resource, prompt cần kiểm tra mức quyền.

    Python

    python
    @tool()
    def delete_product(id: int):
       try:
           check_permissions(role="Admin.Write", request)
       catch:
         pass # client failed authorization, raise authorization error
    

    TypeScript

    typescript
    server.registerTool(
     "delete-product",
     {
       title: Delete a product",
       description: "Deletes a product",
       inputSchema: { id: z.number() }
     },
     async ({ id }) => {
       
       try {
         checkPermissions("Admin.Write", request);
         // todo, send id to productService and remote entry
       } catch(Exception e) {
         console.log("Authorization error, you're not allowed");  
       }
    
       return {
         content: [{ type: "text", text: `Deletected product with id ${id}` }]
       };
     }
    );
    
  • Dùng cách tiếp cận server nâng cao với request handler để giảm số chỗ phải kiểm tra.

    Python

    python
    
    tool_permission = {
       "create_product": ["User.Write", "Admin.Write"],
       "delete_product": ["Admin.Write"]
    }
    
    def has_permission(user_permissions, required_permissions) -> bool:
       # user_permissions: list of permissions the user has
       # required_permissions: list of permissions required for the tool
       return any(perm in user_permissions for perm in required_permissions)
    
    @server.call_tool()
    async def handle_call_tool(
      name: str, arguments: dict[str, str] | None
    ) -> list[types.TextContent]:
     # Assume request.user.permissions is a list of permissions for the user
      user_permissions = request.user.permissions
      required_permissions = tool_permission.get(name, [])
      if not has_permission(user_permissions, required_permissions):
         # Raise error "You don't have permission to call tool {name}"
         raise Exception(f"You don't have permission to call tool {name}")
      # carry on and call tool
      # ...
    

    TypeScript

    typescript
    function hasPermission(userPermissions: string[], requiredPermissions: string[]): boolean {
        if (!Array.isArray(userPermissions) || !Array.isArray(requiredPermissions)) return false;
        // Return true if user has at least one required permission
        
        return requiredPermissions.some(perm => userPermissions.includes(perm));
    }
    
    server.setRequestHandler(CallToolRequestSchema, async (request) => {
       const { params: { name } } = request;
    
       let permissions = request.user.permissions;
    
       if (!hasPermission(permissions, toolPermissions[name])) {
          return new Error(`You don't have permission to call ${name}`);
       }
    
       // carry on..
    });
    

    Lưu ý, bạn cần đảm bảo middleware gán token đã giải mã vào thuộc tính user của request để code trên đơn giản như vậy.

Tổng kết

Đã bàn cách hỗ trợ RBAC nói chung và cho MCP nói riêng, giờ là lúc tự cài đặt bảo mật để chắc bạn đã hiểu các khái niệm.

Bài tập 1: xây MCP server và MCP client dùng basic authentication

Áp dụng những gì đã học về gửi credential qua header.

Lời giải 1: solution.

Bài tập 2: nâng cấp lời giải bài tập 1 sang JWT

Lấy lời giải đầu tiên nhưng lần này cải thiện nó: dùng JWT thay cho basic auth.

Lời giải 2: solution.

Thử thách

Thêm RBAC cho từng tool như mô tả ở phần "Thêm RBAC vào MCP".

Tóm tắt

Hy vọng bạn đã học được nhiều trong bài này, từ không bảo mật gì, tới bảo mật cơ bản, tới JWT và cách thêm nó vào MCP.

Chúng ta đã xây nền tảng vững với JWT tự tạo, nhưng khi mở rộng, ta sẽ chuyển sang mô hình định danh dựa trên chuẩn. Dùng một IdP như Entra hay Keycloak cho phép giao việc phát hành, kiểm tra và quản lý vòng đời token cho một nền tảng đáng tin, để ta tập trung vào logic ứng dụng và trải nghiệm người dùng. Về chủ đề này, khoá gốc có bài nâng cao về Entra.

Bài tiếp theo: Best practice bảo mật MCP.

Nguồn