跳到主要内容
模型上下文协议 (MCP) 中的身份验证可保护对 MCP 服务器公开的敏感资源和操作的访问。如果您的 MCP 服务器处理用户数据或管理操作,身份验证可确保只有获得许可的用户才能访问其端点。 MCP 使用标准化的身份验证流程来建立 MCP 客户端与 MCP 服务器之间的信任。其设计并不侧重于某一个特定的身份验证或身份系统,而是遵循 OAuth 2.1 概述的规范。有关详细信息,请参阅 身份验证规范

什么时候应该使用身份验证?

虽然 MCP 服务器的身份验证是可选的,但在以下情况下强烈建议使用:
  • 您的服务器访问用户特定数据(电子邮件、文档、数据库)
  • 您需要审计谁执行了哪些操作
  • 您的服务器授予对其需要用户同意的 API 的访问权限
  • 您正在为具有严格访问控制的企业环境构建应用
  • 您想要实现针对每个用户的频率限制或使用情况跟踪
本地 MCP 服务器的身份验证对于使用 STDIO 传输的 MCP 服务器,您可以改用基于环境的凭据或直接嵌入在 MCP 服务器中的第三方库提供的凭据。由于基于 STDIO 构建的 MCP 服务器在本地运行,因此在获取用户凭据时,它可以访问一系列灵活的选项,这些选项可能依赖也可能不依赖浏览器内的身份验证和授权流程。相应地,OAuth 流程专为基于 HTTP 的传输而设计,在这种情况下,MCP 服务器是远程托管的,客户端使用 OAuth 来确认用户已被授权访问该远程服务器。

身份验证流程:逐步详解

让我们来看看当客户端想要连接到您受保护的 MCP 服务器时会发生什么
1

初始握手

当您的 MCP 客户端第一次尝试连接时,您的服务器会响应 401 Unauthorized,并告知客户端在哪里可以找到身份验证信息,这些信息包含在 受保护资源元数据 (PRM) 文档中。该文档由 MCP 服务器托管,遵循可预测的路径模式,并通过 WWW-Authenticate 标头中的 resource_metadata 参数提供给客户端。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
  resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
这告知客户端该 MCP 服务器需要身份验证,以及从何处获取启动身份验证流程所需的信息。
2

受保护资源元数据发现

通过指向 PRM 文档的 URI 指针,客户端将获取元数据以了解身份验证服务器、支持的作用域和其他资源信息。数据通常封装在 JSON 对象中,如下所示。
{
  "resource": "https://your-server.com/mcp",
  "authorization_servers": ["https://auth.your-server.com"],
  "scopes_supported": ["mcp:tools", "mcp:resources"]
}
您可以在 RFC 9728 第 3.2 节中查看更全面的示例。
3

身份验证服务器发现

接下来,客户端通过获取身份验证服务器的元数据来发现其功能。如果 PRM 文档列出了多个身份验证服务器,客户端可以决定使用哪一个。选定身份验证服务器后,客户端将构建一个标准的元数据 URI,并向 OpenID Connect (OIDC) 发现OAuth 2.0 身份验证服务器元数据端点(取决于身份验证服务器的支持情况)发出请求,并检索另一组元数据属性,从而得知完成身份验证流程所需的端点。
{
  "issuer": "https://auth.your-server.com",
  "authorization_endpoint": "https://auth.your-server.com/authorize",
  "token_endpoint": "https://auth.your-server.com/token",
  "registration_endpoint": "https://auth.your-server.com/register"
}
4

客户端注册

搞定所有元数据后,客户端现在需要确保已在身份验证服务器上注册。这可以通过两种方式完成。首先,客户端可以在给定的身份验证服务器上预注册,在这种情况下,它可以包含用于完成身份验证流程的嵌入式客户端注册信息。或者,客户端可以使用动态客户端注册 (DCR) 向身份验证服务器进行动态注册。后一种情况要求身份验证服务器支持 DCR。如果身份验证服务器支持 DCR,客户端将向 registration_endpoint 发送包含其信息的请求:
{
  "client_name": "My MCP Client",
  "redirect_uris": ["https://:3000/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}
如果注册成功,身份验证服务器将返回一个包含客户端注册信息的 JSON 对象。
无 DCR 或预注册的情况如果 MCP 客户端连接到一个不支持 DCR 且客户端未在该身份验证服务器上预注册的 MCP 服务器,则客户端开发人员有责任提供一种方式,让最终用户手动输入客户端信息。
5

用户授权

客户端现在需要打开浏览器访问 /authorize 端点,用户可以在此处登录并授予所需的权限。身份验证服务器随后将重定向回客户端,并附带一个授权码,客户端用该授权码交换令牌。
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "def502...",
  "token_type": "Bearer",
  "expires_in": 3600
}
访问令牌是客户端用于对 MCP 服务器请求进行身份验证的内容。此步骤遵循标准的 带 PKCE 的 OAuth 2.1 授权码 规范。
6

发起经过身份验证的请求

最后,客户端可以使用嵌入在 Authorization 标头中的访问令牌向您的 MCP 服务器发起请求。
GET /mcp HTTP/1.1
Host: your-server.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
MCP 服务器需要验证令牌,如果令牌有效且具有所需权限,则处理该请求。

实现示例

为了开始实际实现,我们将使用托管在 Docker 容器中的 Keycloak 身份验证服务器。Keycloak 是一个开源身份验证服务器,可以轻松部署在本地进行测试和实验。 请确保您已下载并安装 Docker Desktop。我们需要它在开发机器上部署 Keycloak。

Keycloak 设置

在您的终端应用程序中,运行以下命令以启动 Keycloak 容器
docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak start-dev
此命令将在本地拉取 Keycloak 容器镜像并引导基本配置。它将在端口 8080 上运行,并拥有一个用户名为 admin、密码为 admin 的管理员用户。
非生产环境使用上述配置可能适用于测试和实验;但是,您绝不应在生产环境中使用它。有关如何为需要可靠性、安全性和高可用性的场景部署身份验证服务器的更多详细信息,请参阅 为生产环境配置 Keycloak 指南。
您可以通过浏览器访问 https://:8080 进入 Keycloak 身份验证服务器。
Keycloak admin dashboard authentication dialog.
在使用默认配置运行时,Keycloak 已经支持我们为 MCP 服务器所需的许多功能,包括动态客户端注册。您可以通过查看 OIDC 配置来检查这一点,地址为:
https://:8080/realms/master/.well-known/openid-configuration
我们还需要设置 Keycloak 以支持我们的作用域,并允许我们的主机(本地机器)动态注册客户端,因为默认策略限制了匿名的动态客户端注册。 进入 Keycloak 控制面板中的 Client scopes(客户端作用域),并创建一个新的 mcp:tools 作用域。我们将使用它来访问 MCP 服务器上的所有工具。
Configuring Keycloak scopes.
创建作用域后,请确保将其类型指定为 Default,并打开 Include in token scope 开关,因为令牌验证需要此设置。 现在让我们也为 Keycloak 签发的令牌设置一个 audience(受众)。配置受众很重要,因为它将预期的目的地直接嵌入到签发的访问令牌中。这有助于您的 MCP 服务器验证它收到的令牌确实是发给它的,而不是发给其他 API 的。这是避免令牌透传场景的关键。 为此,打开您的 mcp:tools 客户端作用域,点击 Mappers(映射器),然后点击 Configure a new mapper(配置新映射器)。选择 Audience
Configuring an audience for a token in Keycloak.
Name 中使用 audience-config。在 Included Custom Audience 中添加一个值,设置为 https://:3000。这将是我们测试服务器的 URI。
非生产环境使用上述受众配置仅用于测试。对于生产场景,需要进行额外的设置和配置,以确保签发令牌的受众受到适当限制。具体而言,受众需要基于从客户端传递的资源参数,而不是固定值。
现在,导航到 Clients(客户端),然后是 Client registration(客户端注册),最后是 Trusted Hosts(信任主机)。禁用 Client URIs Must Match(客户端 URI 必须匹配)设置,并添加您正在进行测试的主机。您可以通过在 Linux 或 macOS 上运行 ifconfig 命令,或在 Windows 上运行 ipconfig 来获取当前主机的 IP。您可以通过查看 Keycloak 日志中类似于 Failed to verify remote host : 192.168.215.1 的行来查看需要添加的 IP 地址。检查该 IP 地址是否与您的主机相关联。根据您的 Docker 设置,这可能是一个桥接网络。
Setting up client registration details in Keycloak.
获取主机信息如果您从容器运行 Keycloak,也可以在容器日志的终端中看到主机 IP。
最后,我们需要注册一个新客户端,供 MCP 服务器本身使用,以便与 Keycloak 通信进行诸如 令牌内省 之类的操作。操作步骤如下:
  1. 进入 Clients
  2. 点击 Create client
  3. 给您的客户端起一个唯一的 Client ID 并点击 Next
  4. 开启 Client authentication 并点击 Next
  5. 点击 Save
值得注意的是,令牌内省只是验证令牌的可用方法之一。这也可以借助特定于每种语言和平台的独立库来完成。 打开客户端详情时,进入 Credentials 并记下 Client Secret
Creating a new client in Keycloak.
处理密钥切勿将客户端凭据直接嵌入代码中。我们建议使用环境变量或专门的密钥存储解决方案。
配置好 Keycloak 后,每次触发身份验证流程时,您的 MCP 服务器都会收到如下令牌:
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1TjcxMGw1WW5MWk13WGZ1VlJKWGtCS3ZZMzZzb3JnRG5scmlyZ2tlTHlzIn0.eyJleHAiOjE3NTU1NDA4MTcsImlhdCI6MTc1NTU0MDc1NywiYXV0aF90aW1lIjoxNzU1NTM4ODg4LCJqdGkiOiJvbnJ0YWM6YjM0MDgwZmYtODQwNC02ODY3LTgxYmUtMTIzMWI1MDU5M2E4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJzdWIiOiIzM2VkNmM2Yi1jNmUwLTQ5MjgtYTE2MS1mMmY2OWM3YTAzYjkiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiI3OTc1YTViNi04YjU5LTRhODUtOWNiYS04ZmFlYmRhYjg5NzQiLCJzaWQiOiI4ZjdlYzI3Ni0zNThmLTRjY2MtYjMxMy1kYjA4MjkwZjM3NmYiLCJzY29wZSI6Im1jcDp0b29scyJ9.P5xCRtXORly0R0EXjyqRCUx-z3J4uAOWNAvYtLPXroykZuVCCJ-K1haiQSwbURqfsVOMbL7jiV-sD6miuPzI1tmKOkN_Yct0Vp-azvj7U5rEj7U6tvPfMkg2Uj_jrIX0KOskyU2pVvGZ-5BgqaSvwTEdsGu_V3_E0xDuSBq2uj_wmhqiyTFm5lJ1WkM3Hnxxx1_AAnTj7iOKMFZ4VCwMmk8hhSC7clnDauORc0sutxiJuYUZzxNiNPkmNeQtMCGqWdP1igcbWbrfnNXhJ6NswBOuRbh97_QraET3hl-CNmyS6C72Xc0aOwR_uJ7xVSBTD02OaQ1JA6kjCATz30kGYg
解码后,它看起来像这样:
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "5N710l5YnLZMwXfuVRJXkBKvY36sorgDnlrirgkeLys"
}.{
  "exp": 1755540817,
  "iat": 1755540757,
  "auth_time": 1755538888,
  "jti": "onrtac:b34080ff-8404-6867-81be-1231b50593a8",
  "iss": "https://:8080/realms/master",
  "aud": "https://:3000",
  "sub": "33ed6c6b-c6e0-4928-a161-f2f69c7a03b9",
  "typ": "Bearer",
  "azp": "7975a5b6-8b59-4a85-9cba-8faebdab8974",
  "sid": "8f7ec276-358f-4ccc-b313-db08290f376f",
  "scope": "mcp:tools"
}.[Signature]
嵌入的受众注意令牌中嵌入的 aud 声明——它目前被设置为测试 MCP 服务器的 URI,并且是从我们之前配置的作用域中推断出来的。这在我们的实现验证中非常重要。

MCP 服务器设置

我们现在将设置 MCP 服务器以使用本地运行的 Keycloak 身份验证服务器。根据您的编程语言偏好,您可以使用受支持的 MCP SDK 之一。 为了测试目的,我们将创建一个极其简单的 MCP 服务器,它公开两个工具——一个用于加法,另一个用于乘法。服务器将要求身份验证才能访问这些工具。
您可以在 示例代码库 中查看完整的 TypeScript 项目。在运行下面的代码之前,请确保您有一个包含以下内容的 .env 文件:
# Server host/port
HOST=localhost
PORT=3000

# Auth server location
AUTH_HOST=localhost
AUTH_PORT=8080
AUTH_REALM=master

# Keycloak OAuth client credentials
OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
OAUTH_CLIENT_IDOAUTH_CLIENT_SECRET 与我们之前创建的 MCP 服务器客户端相关联。除了实现 MCP 身份验证规范外,下面的服务器还通过 Keycloak 执行令牌内省,以确保它从客户端接收到的令牌有效。它还实现了基本日志记录,以便您轻松诊断任何问题。
import "dotenv/config";
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import cors from "cors";
import {
  mcpAuthMetadataRouter,
  getOAuthProtectedResourceMetadataUrl,
} from "@modelcontextprotocol/sdk/server/auth/router.js";
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
const CONFIG = {
  host: process.env.HOST || "localhost",
  port: Number(process.env.PORT) || 3000,
  auth: {
    host: process.env.AUTH_HOST || process.env.HOST || "localhost",
    port: Number(process.env.AUTH_PORT) || 8080,
    realm: process.env.AUTH_REALM || "master",
    clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
    clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
  },
};

function createOAuthUrls() {
  const authBaseUrl = new URL(
    `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
  );
  return {
    issuer: authBaseUrl.toString(),
    introspection_endpoint: new URL(
      "protocol/openid-connect/token/introspect",
      authBaseUrl,
    ).toString(),
    authorization_endpoint: new URL(
      "protocol/openid-connect/auth",
      authBaseUrl,
    ).toString(),
    token_endpoint: new URL(
      "protocol/openid-connect/token",
      authBaseUrl,
    ).toString(),
  };
}

function createRequestLogger() {
  return (req: any, res: any, next: any) => {
    const start = Date.now();
    res.on("finish", () => {
      const ms = Date.now() - start;
      console.log(
        `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
      );
    });
    next();
  };
}

const app = express();

app.use(
  express.json({
    verify: (req: any, _res, buf) => {
      req.rawBody = buf?.toString() ?? "";
    },
  }),
);

app.use(
  cors({
    origin: "*",
    exposedHeaders: ["Mcp-Session-Id"],
  }),
);

app.use(createRequestLogger());

const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
const oauthUrls = createOAuthUrls();

const oauthMetadata: OAuthMetadata = {
  ...oauthUrls,
  response_types_supported: ["code"],
};

const tokenVerifier = {
  verifyAccessToken: async (token: string) => {
    const endpoint = oauthMetadata.introspection_endpoint;

    if (!endpoint) {
      console.error("[auth] no introspection endpoint in metadata");
      throw new Error("No token verification endpoint available in metadata");
    }

    const params = new URLSearchParams({
      token: token,
      client_id: CONFIG.auth.clientId,
    });

    if (CONFIG.auth.clientSecret) {
      params.set("client_secret", CONFIG.auth.clientSecret);
    }

    let response: Response;
    try {
      response = await fetch(endpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: params.toString(),
      });
    } catch (e) {
      console.error("[auth] introspection fetch threw", e);
      throw e;
    }

    if (!response.ok) {
      const txt = await response.text();
      console.error("[auth] introspection non-OK", { status: response.status });

      try {
        const obj = JSON.parse(txt);
        console.log(JSON.stringify(obj, null, 2));
      } catch {
        console.error(txt);
      }
      throw new Error(`Invalid or expired token: ${txt}`);
    }

    let data: any;
    try {
      data = await response.json();
    } catch (e) {
      const txt = await response.text();
      console.error("[auth] failed to parse introspection JSON", {
        error: String(e),
        body: txt,
      });
      throw e;
    }

    if (data.active === false) {
      throw new Error("Inactive token");
    }

    if (!data.aud) {
      throw new Error("Resource indicator (aud) missing");
    }

    const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
    const allowed = audiences.some((a) =>
      checkResourceAllowed({
        requestedResource: a,
        configuredResource: mcpServerUrl,
      }),
    );
    if (!allowed) {
      throw new Error(
        `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
      );
    }

    return {
      token,
      clientId: data.client_id,
      scopes: data.scope ? data.scope.split(" ") : [],
      expiresAt: data.exp,
    };
  },
};
app.use(
  mcpAuthMetadataRouter({
    oauthMetadata,
    resourceServerUrl: mcpServerUrl,
    scopesSupported: ["mcp:tools"],
    resourceName: "MCP Demo Server",
  }),
);

const authMiddleware = requireBearerAuth({
  verifier: tokenVerifier,
  requiredScopes: [],
  resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
});

const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

function createMcpServer() {
  const server = new McpServer({
    name: "example-server",
    version: "1.0.0",
  });

  server.registerTool(
    "add",
    {
      title: "Addition Tool",
      description: "Add two numbers together",
      inputSchema: {
        a: z.number().describe("First number to add"),
        b: z.number().describe("Second number to add"),
      },
    },
    async ({ a, b }) => ({
      content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
    }),
  );

  server.registerTool(
    "multiply",
    {
      title: "Multiplication Tool",
      description: "Multiply two numbers together",
      inputSchema: {
        x: z.number().describe("First number to multiply"),
        y: z.number().describe("Second number to multiply"),
      },
    },
    async ({ x, y }) => ({
      content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
    }),
  );

  return server;
}

const mcpPostHandler = async (req: express.Request, res: express.Response) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  let transport: StreamableHTTPServerTransport;

  if (sessionId && transports[sessionId]) {
    transport = transports[sessionId];
  } else if (!sessionId && isInitializeRequest(req.body)) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (sessionId) => {
        transports[sessionId] = transport;
      },
    });

    transport.onclose = () => {
      if (transport.sessionId) {
        delete transports[transport.sessionId];
      }
    };

    const server = createMcpServer();
    await server.connect(transport);
  } else {
    res.status(400).json({
      jsonrpc: "2.0",
      error: {
        code: -32000,
        message: "Bad Request: No valid session ID provided",
      },
      id: null,
    });
    return;
  }

  await transport.handleRequest(req, res, req.body);
};

const handleSessionRequest = async (
  req: express.Request,
  res: express.Response,
) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  if (!sessionId || !transports[sessionId]) {
    res.status(400).send("Invalid or missing session ID");
    return;
  }

  const transport = transports[sessionId];
  await transport.handleRequest(req, res);
};

app.post("/", authMiddleware, mcpPostHandler);
app.get("/", authMiddleware, handleSessionRequest);
app.delete("/", authMiddleware, handleSessionRequest);

app.listen(CONFIG.port, CONFIG.host, () => {
  console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
  console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
  console.log(
    `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
  );
});
运行服务器时,您可以通过提供 MCP 服务器端点将其添加到您的 MCP 客户端(如 Visual Studio Code)。有关在 TypeScript 中实现 MCP 服务器的更多详细信息,请参阅 TypeScript SDK 文档

测试 MCP 服务器

为了测试目的,我们将使用 Visual Studio Code,但任何支持 MCP 和新身份验证规范的客户端都适用。 按下 Cmd + Shift + P 并选择 MCP: Add server…。选择 HTTP 并输入 https://:3000。给服务器起一个在 Visual Studio Code 内部使用的唯一名称。在 mcp.json 中,您现在应该看到如下条目:
"my-mcp-server-18676652": {
  "url": "https://:3000",
  "type": "http"
}
连接时,您将被引导至浏览器,系统会提示您同意 Visual Studio Code 访问 mcp:tools 作用域。
Keycloak consent form for VS Code.
同意后,您将在 mcp.json 中服务器条目的正上方看到列出的工具。
Tools listed in VS Code.
您可以借助聊天视图中的 # 符号来调用各个工具。
Invoking MCP tools in VS Code.

常见陷阱及如何避免

如需全面的安全指南,包括攻击向量、缓解策略和实现最佳实践,请务必阅读 安全最佳实践。下面列出了一些关键问题。
  • 不要自己实现令牌验证或身份验证逻辑。对于令牌验证或身份验证决策等内容,请使用现成的、经过充分测试且安全的库。除非您是安全专家,否则从头开始做所有事情意味着您更有可能实现错误。
  • 使用短效访问令牌。根据所使用的身份验证服务器,此设置可能是可定制的。我们建议不要使用长效令牌——如果恶意行为者窃取了它们,他们将能够在更长时间内维持访问权限。
  • 始终验证令牌。您的服务器收到令牌并不意味着该令牌有效或它是发给您的服务器的。始终验证您的 MCP 服务器从客户端获取的内容是否符合所需的约束条件。
  • 将令牌存储在安全的加密存储中。在某些情况下,您可能需要在服务器端缓存令牌。如果是这种情况,请确保存储具有正确的访问控制,并且不会被有权访问您服务器的恶意方轻易窃取。您还应该实现稳健的缓存逐出策略,以确保您的 MCP 服务器不会重复使用过期或无效的令牌。
  • 在生产环境中强制使用 HTTPS。除了开发期间的 localhost 外,不要通过普通 HTTP 接受令牌或重定向回调。
  • 最小特权作用域。不要使用全能作用域。尽可能按工具或功能拆分访问权限,并在资源服务器上按路由/工具验证所需的作用域。
  • 不要记录凭据。切勿记录 Authorization 标头、令牌、代码或密钥。清除查询字符串和标头。脱敏结构化日志中的敏感字段。
  • 分离应用与资源服务器凭据。不要将您的 MCP 服务器的客户端密钥重新用于端用户流程。将所有密钥存储在适当的密钥管理器中,而不是源代码控制中。
  • 返回正确的质询。在 401 错误时,包含带有 Bearerrealmresource_metadataWWW-Authenticate,以便客户端可以发现如何进行身份验证。
  • DCR(动态客户端注册)控制。如果启用了该功能,请注意特定于您组织的约束,例如信任主机、所需的审核和经过审计的注册。未经过身份验证的 DCR 意味着任何人都可以向您的身份验证服务器注册任何客户端。
  • 多租户/领域混淆。除非明确是多租户,否则固定到单个发行者/租户。拒绝来自其他领域的令牌,即使它们是由同一个身份验证服务器签名的。
  • 受众/资源指示器误用。不要配置或接受通用的受众(如 api)或无关的资源。要求受众/资源与您配置的服务器相匹配。
  • 错误详情泄漏。向客户端返回通用消息,但在内部记录带有相关 ID 的详细原因,以帮助故障排除而不泄露内部信息。
  • 会话标识符强化。将 Mcp-Session-Id 视为不可信输入;切勿将身份验证与其绑定。在身份验证变更时重新生成,并在服务器端验证生命周期。
MCP 身份验证建立在以下成熟标准之上: 有关更多详细信息,请参考: 了解这些标准将帮助您正确实现身份验证并在出现问题时进行故障排除。