跳到主要内容
模型上下文协议 (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 指针,客户端将获取元数据,以了解授权服务器、支持的范围 (scopes) 以及其他资源信息。这些数据通常封装在 JSON blob 中,如下所示。
{
  "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) DiscoveryOAuth 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 blob。
无 DCR 或预注册的情况如果 MCP 客户端连接到的 MCP 服务器所使用的授权服务器不支持 DCR,且客户端未进行预注册,则客户端开发人员有责任为最终用户提供手动输入客户端信息的界面。
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 的管理员账户。
不适用于生产环境上述配置可能适用于测试和实验,但您绝不应将其用于生产环境。有关如何为需要可靠性、安全性和高可用性的场景部署授权服务器的更多详细信息,请参阅 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 设置,并添加您进行测试的主机。您可以通过在 Linux 或 macOS 上运行 ifconfig 命令,或在 Windows 上运行 ipconfig 来获取当前主机的 IP。您可以通过查看 Keycloak 日志中类似 Failed to verify remote host : 192.168.215.1 的行来查看需要添加的 IP 地址。请检查该 IP 地址是否与您的主机关联。根据您的 Docker 设置,这可能是网桥网络的 IP。
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 服务器的客户端密钥进行最终用户流程。将所有密钥存储在合适的密钥管理器中,而不是源代码控制中。
  • 返回正确的质询 (Challenges)。在 401 响应中,应包含带有 Bearerrealmresource_metadataWWW-Authenticate 标头,以便客户端能够发现如何进行身份验证。
  • DCR (动态客户端注册) 控制。如果启用了 DCR,请注意特定于组织的约束,例如受信任的主机、所需的审查和已审计的注册。未经身份验证的 DCR 意味着任何人都可以向您的授权服务器注册任何客户端。
  • 多租户/领域 (Realm) 混淆。除非明确为多租户,否则应绑定到单一颁发者/租户。拒绝来自其他领域的令牌,即使它们是由同一个授权服务器签名的。
  • 受众/资源指示符误用。不要配置或接受通用受众(如 api)或不相关的资源。要求受众/资源必须与您配置的服务器匹配。
  • 错误详情泄漏。向客户端返回通用消息,但在内部记录详细原因(包含关联 ID),以辅助排查问题,同时避免暴露内部细节。
  • 会话标识符强化。将 Mcp-Session-Id 视为不受信任的输入;切勿将其与授权挂钩。在身份验证发生更改时重新生成,并在服务器端验证其生命周期。
MCP 授权基于以下成熟标准构建: 有关更多详细信息,请参阅: 了解这些标准将帮助您正确实现授权,并在问题出现时进行排查。