主题
理解 MCP 中的授权
了解如何使用 OAuth 2.1 为 MCP 服务端实现安全授权,以保护敏感资源与操作
Model Context Protocol (MCP) 中的授权用于保护对 MCP 服务端所暴露的敏感资源与操作的访问。如果你的 MCP 服务端处理用户数据或管理操作,授权可确保只有被允许的用户才能访问其端点。
MCP 使用标准化的授权流程,在 MCP 客户端(连接器)与 MCP 服务端之间建立信任。其设计并不聚焦于某一特定的授权或身份系统,而是遵循为 OAuth 2.1 所制定的约定。详细信息请参见授权规范。
何时应使用授权?
虽然 MCP 服务端的授权是可选的,但在以下情况下强烈建议使用:
- 你的服务端访问用户特定数据(电子邮件、文档、数据库)
- 你需要审计谁执行了哪些操作
- 你的服务端授予对其需要用户同意的 API 的访问权限
- 你正在为具有严格访问控制的企业环境进行构建
- 你希望按用户实施速率限制或使用量跟踪
TIP
本地 MCP 服务端的授权
对于使用 STDIO 传输的 MCP 服务端,你可以改用基于环境的凭据,或由直接嵌入 MCP 服务端的第三方库提供的凭据。由于基于 STDIO 构建的 MCP 服务端在本地运行,在获取用户凭据方面它有一系列灵活的选择,这些选择可能依赖也可能不依赖浏览器内的认证与授权流程。
而 OAuth 流程则是为基于 HTTP 的传输而设计的,在这种场景下 MCP 服务端是远程托管的,客户端使用 OAuth 来确认某个用户已被授权访问该远程服务端。
授权流程:分步说明
让我们逐步了解当客户端想要连接到你所保护的 MCP 服务端时会发生什么:
初始握手
当你的 MCP 客户端首次尝试连接时,你的服务端会以 `401 Unauthorized` 响应,并告知客户端在哪里可以找到授权信息,这些信息记录在[受保护资源元数据 (PRM) 文档](https://datatracker.ietf.org/doc/html/rfc9728)中。该文档由 MCP 服务端托管,遵循可预测的路径模式,并通过 `WWW-Authenticate` 头中的 `resource_metadata` 参数提供给客户端。
```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
```
这告诉客户端该 MCP 服务端需要授权,以及从哪里获取启动授权流程所需的信息。
受保护资源元数据发现
有了指向 PRM 文档的 URI 指针,客户端将获取该元数据,以了解授权服务器、支持的 scope 以及其他资源信息。这些数据通常封装在一个 JSON blob 中,类似于下面这个。
```json
{
"resource": "https://your-server.com/mcp",
"authorization_servers": ["https://auth.your-server.com"],
"scopes_supported": ["mcp:tools", "mcp:resources"]
}
```
你可以在 [RFC 9728 第 3.2 节](https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata-r)中看到一个更完整的示例。
授权服务器发现
接下来,客户端通过获取授权服务器的元数据来发现它能做什么。如果 PRM 文档列出了多个授权服务器,客户端可以决定使用哪一个。
选定授权服务器后,客户端将构造一个标准元数据 URI,并向 [OpenID Connect (OIDC) Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) 或 [OAuth 2.0 Auth Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) 端点发起请求(取决于授权服务器的支持情况),并获取另一组元数据属性,从而了解完成授权流程所需的各个端点。
```json
{
"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"
}
```
客户端注册
所有元数据都处理完毕后,客户端现在需要确保自己已在授权服务器上注册。这可以通过两种方式完成。
首先,客户端可以**预先注册**到某个给定的授权服务器,在这种情况下,它可以内置客户端注册信息,用于完成授权流程。
或者,客户端可以使用**动态客户端注册** (DCR) 将自己动态注册到授权服务器。后一种场景要求授权服务器支持 DCR。如果授权服务器确实支持 DCR,客户端将向其 `registration_endpoint` 发送包含自身信息的请求:
```json
{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:3000/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}
```
如果注册成功,授权服务器将返回一个包含客户端注册信息的 JSON blob。
TIP
不使用 DCR 或预注册
如果 MCP 客户端连接的 MCP 服务端所使用的授权服务器不支持 DCR,且该客户端未在所述授权服务器上预注册,则客户端开发者有责任提供一种方式,让最终用户手动输入客户端信息。
用户授权
客户端现在需要打开浏览器并访问 `/authorize` 端点,用户可以在其中登录并授予所需的权限。随后,授权服务器将重定向回客户端,并附带一个授权码,客户端用它来换取令牌:
```json
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "def502...",
"token_type": "Bearer",
"expires_in": 3600
}
```
访问令牌是客户端用来向 MCP 服务端发送请求时进行身份验证的凭证。此步骤遵循标准的 [OAuth 2.1 authorization code with PKCE](https://oauth.net/2/grant-types/authorization-code/) 约定。
发起已认证请求
最后,客户端可以使用嵌入在 `Authorization` 头中的访问令牌向你的 MCP 服务端发起请求:
```http
GET /mcp HTTP/1.1
Host: your-server.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
```
MCP 服务端需要验证该令牌,并在令牌有效且具有所需权限时处理该请求。
实现示例
为了开始一个实际的实现,我们将使用一个托管在 Docker 容器中的 Keycloak 授权服务器。Keycloak 是一个开源授权服务器,可以轻松地在本地部署以进行测试和实验。
请确保你下载并安装了 Docker Desktop。我们需要它来在开发机器上部署 Keycloak。
Keycloak 设置
在终端应用程序中,运行以下命令以启动 Keycloak 容器:
bash
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-dev1
此命令将在本地拉取 Keycloak 容器镜像并引导基本配置。它将在端口 8080 上运行,并拥有一个用户名为 admin、密码为 admin 的用户。
WARNING
不适用于生产环境
上述配置可能适用于测试和实验;但是,你绝不应在生产环境中使用它。有关在需要可靠性、安全性和高可用性的场景中如何部署授权服务器的更多详细信息,请参阅 Configuring Keycloak for production 指南。
你将能够通过浏览器在 http://localhost:8080 访问 Keycloak 授权服务器。

在使用默认配置运行时,Keycloak 已经支持我们为 MCP 服务端所需的许多能力,包括动态客户端注册。你可以通过查看 OIDC 配置来验证这一点,该配置位于:
http
http://localhost:8080/realms/master/.well-known/openid-configuration1
我们还需要设置 Keycloak 以支持我们的作用域,并允许我们的宿主(本地机器)动态注册客户端,因为默认策略限制了匿名动态客户端注册。
转到 Keycloak 仪表板中的 Client scopes,并创建一个新的 mcp:tools 作用域。我们将使用它来访问 MCP 服务端上的所有工具。

创建作用域后,请确保将其类型分配为 Default,并打开 Include in token scope 开关,因为令牌验证将需要它。
现在,让我们为 Keycloak 签发的令牌设置一个 audience。配置 audience 很重要,因为它将预期目标直接嵌入到签发的访问令牌中。这有助于你的 MCP 服务端验证它所收到的令牌确实是发给它的,而不是发给其他 API 的。这是帮助避免 Token 透传场景的关键。
为此,打开你的 mcp:tools 客户端作用域,点击 Mappers,然后点击 Configure a new mapper。选择 Audience。

对于 Name,使用 audience-config。为 Included Custom Audience 添加一个值,设置为 http://localhost:3000。这将是我们测试服务端的 URI。
WARNING
不适用于生产环境
上述 audience 配置仅用于测试。对于生产场景,将需要额外的设置和配置,以确保签发的令牌的 audience 受到适当约束。具体来说,audience 需要基于从客户端传递的 resource 参数,而不是固定值。
现在,导航到 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 设置,这可能是一个桥接网络。

WARNING
获取 Host
如果你从容器中运行 Keycloak,你也可以从容器日志的终端中看到 host IP。
最后,我们需要注册一个新客户端,用于与 MCP 服务端本身 配合,以便与 Keycloak 通信,例如进行 token introspection。为此:
- 前往 Clients。
- 点击 Create client。
- 为你的客户端指定一个唯一的 Client ID,然后点击 Next。
- 启用 Client authentication,然后点击 Next。
- 点击 Save。
值得注意的是,token introspection 只是验证 token 的可用方法之一。这也可以借助特定于每种语言和平台的独立库来完成。
当你打开客户端详情时,前往 Credentials 并记下 Client Secret。

WARNING
处理密钥
切勿将客户端凭据直接嵌入到你的代码中。我们建议使用环境变量或专门的密钥存储解决方案。
配置好 Keycloak 后,每次触发授权流程时,你的 MCP 服务端都会收到一个类似这样的 token:
text
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1TjcxMGw1WW5MWk13WGZ1VlJKWGtCS3ZZMzZzb3JnRG5scmlyZ2tlTHlzIn0.eyJleHAiOjE3NTU1NDA4MTcsImlhdCI6MTc1NTU0MDc1NywiYXV0aF90aW1lIjoxNzU1NTM4ODg4LCJqdGkiOiJvbnJ0YWM6YjM0MDgwZmYtODQwNC02ODY3LTgxYmUtMTIzMWI1MDU5M2E4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJzdWIiOiIzM2VkNmM2Yi1jNmUwLTQ5MjgtYTE2MS1mMmY2OWM3YTAzYjkiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiI3OTc1YTViNi04YjU5LTRhODUtOWNiYS04ZmFlYmRhYjg5NzQiLCJzaWQiOiI4ZjdlYzI3Ni0zNThmLTRjY2MtYjMxMy1kYjA4MjkwZjM3NmYiLCJzY29wZSI6Im1jcDp0b29scyJ9.P5xCRtXORly0R0EXjyqRCUx-z3J4uAOWNAvYtLPXroykZuVCCJ-K1haiQSwbURqfsVOMbL7jiV-sD6miuPzI1tmKOkN_Yct0Vp-azvj7U5rEj7U6tvPfMkg2Uj_jrIX0KOskyU2pVvGZ-5BgqaSvwTEdsGu_V3_E0xDuSBq2uj_wmhqiyTFm5lJ1WkM3Hnxxx1_AAnTj7iOKMFZ4VCwMmk8hhSC7clnDauORc0sutxiJuYUZzxNiNPkmNeQtMCGqWdP1igcbWbrfnNXhJ6NswBOuRbh97_QraET3hl-CNmyS6C72Xc0aOwR_uJ7xVSBTD02OaQ1JA6kjCATz30kGYg1
解码后,它看起来会是这样:
json
{
"alg": "RS256",
"typ": "JWT",
"kid": "5N710l5YnLZMwXfuVRJXkBKvY36sorgDnlrirgkeLys"
}.{
"exp": 1755540817,
"iat": 1755540757,
"auth_time": 1755538888,
"jti": "onrtac:b34080ff-8404-6867-81be-1231b50593a8",
"iss": "http://localhost:8080/realms/master",
"aud": "http://localhost: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]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
WARNING
内嵌的 Audience
注意 token 中内嵌的 aud 声明——它当前被设置为测试 MCP 服务端的 URI,并且是从我们之前配置的 scope 推断出来的。这在我们的实现中对于验证非常重要。
MCP 服务端设置
我们现在将设置 MCP 服务端,使其使用本地运行的 Keycloak 授权服务器。根据你的编程语言偏好,你可以使用受支持的 MCP SDKs 之一。
出于测试目的,我们将创建一个极其简单的 MCP 服务端,它暴露两个工具——一个用于加法,另一个用于乘法。该服务端将要求授权才能访问这些工具。
你可以在 [sample repository](https://github.com/localden/min-ts-mcp-auth) 中查看完整的 TypeScript 项目。
在运行下面的代码之前,请确保你有一个包含以下内容的 `.env` 文件:
```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_ID` 和 `OAUTH_CLIENT_SECRET` 与我们之前创建的 MCP 服务端客户端相关联。
除了实现 MCP 授权规范外,下面的服务端还通过 Keycloak 进行 token introspection,以确保它从客户端收到的 token 是有效的。它还实现了基本的日志记录,以便你轻松诊断任何问题。
```typescript
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) => {
try {
return checkResourceAllowed({
requestedResource: a,
configuredResource: mcpServerUrl,
});
} catch {
// Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
// Those are never our resource, so treat them as "no match" instead of crashing.
return false;
}
});
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 并输入 http://localhost:3000。为该服务端指定一个在 Visual Studio Code 内使用的唯一名称。此时在 mcp.json 中你应该会看到类似这样的条目:
json
"my-mcp-server-18676652": {
"url": "http://localhost:3000",
"type": "http"
}1
2
3
4
2
3
4
连接时,你将被带到浏览器,在那里系统会提示你同意 Visual Studio Code 访问 mcp:tools 作用域。

同意后,你会在 mcp.json 中服务端条目的正上方看到列出的工具。

你可以借助聊天视图中的 # 符号来调用各个工具。

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