OAuth 客户端凭据

OAuth 客户端凭据

社区译文;来源: MCP 80041ab0

OAuth 客户端凭据扩展(io.modelcontextprotocol/oauth-client-credentials)为 MCP 增加对 OAuth 2.0 客户端凭据流程的支持,使自动化系统无需交互式用户授权即可连接 MCP 服务器。

规范

OAuth 客户端凭据扩展的完整技术规范。

它是什么

标准 MCP 授权流程要求用户通过交互方式批准访问:浏览器打开,用户登录并授予权限。该流程适合有人参与的场景,但在没有用户在场时无法工作。

OAuth 客户端凭据扩展允许客户端使用应用级凭据(客户端 ID 和密钥,或签名 JWT 断言)而非委托用户凭据进行身份验证,从而解决这一问题。客户端直接向授权服务器证明自身身份,授权服务器无需浏览器重定向或用户交互即可签发访问令牌。

何时使用

以下情况适合使用 OAuth 客户端凭据:

  • 后台服务需要按计划或响应事件调用 MCP 工具,且没有用户在场
  • CI/CD 流水线在自动构建、测试或部署工作流中调用 MCP 服务器
  • 服务器间集成连接两个没有最终用户参与的后端系统
  • 守护进程或长时间运行的 Worker 需要持续访问 MCP 资源

如果集成中有需要明确授权访问的人类用户,请改用标准 MCP 授权流程。

工作原理

该扩展支持两种凭据格式:

JWT Bearer 断言(推荐)

RFC 7523 定义的 JWT Bearer 断言允许客户端使用私钥签名令牌,并将其作为身份证明提交。授权服务器使用客户端已注册的公钥验证签名。

  sequenceDiagram
    participant Client
    participant AS as Authorization Server
    participant MCP as MCP Server

    Client->>AS: POST /token<br/>grant_type=urn:ietf:params:<br/>oauth:grant-type:jwt-bearer<br/>assertion=<signed JWT>
    AS-->>Client: access_token
    Client->>MCP: MCP request (Bearer token)

JWT 断言通常包含:

  • iss:客户端 ID(签发者)
  • sub:客户端 ID(被验证身份的主体)
  • aud:授权服务器令牌端点 URL
  • exp:过期时间
  • iat:签发时间

客户端密钥

对于较简单的部署,该扩展还支持使用 client_id 和 client_secret 的标准客户端凭据流程。客户端将凭据直接发送到授权服务器的令牌端点,并接收返回的访问令牌。

  sequenceDiagram
    participant Client
    participant AS as Authorization Server
    participant MCP as MCP Server

    Client->>AS: POST /token<br/>grant_type=client_credentials<br/>client_id + client_secret
    AS-->>Client: access_token
    Client->>MCP: MCP request (Bearer token)

客户端密钥是无需用户交互即可授予访问权限的长期凭据。如果密钥泄露,攻击者可以冒充你的应用,在密钥轮换前持续进行静默身份验证。为降低风险:

  • 将密钥存储在密钥管理器中,绝不要放入源代码或纳入版本控制的环境文件。
  • 按固定计划轮换密钥,并在怀疑密钥泄露后立即轮换。
  • 将凭据的作用域限制为所需的最小权限。
  • 尽可能优先使用 JWT 断言;它们有效期较短,而且无需传输签名密钥。

实现指南

对于 MCP 客户端

要使用 OAuth 客户端凭据扩展,客户端必须:

声明支持

在每个请求的能力中包含该扩展:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "...",
  "params": {
    // Other fields...
    "_meta": {
      // Other fields...
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/oauth-client-credentials": {},
        },
      },
    },
  },
}

获取访问令牌

连接 MCP 服务器前,使用客户端凭据许可从授权服务器请求令牌。

携带令牌

在发往 MCP 服务器的 HTTP 请求的 Authorization 标头中传递令牌:

Authorization: Bearer <access_token>

处理令牌刷新

客户端凭据令牌的有效期通常短于用户委托令牌。请实现令牌刷新逻辑,在令牌过期前获取新令牌。

对于 MCP 服务器

要接受客户端凭据令牌,服务器必须:

验证令牌

对每个请求,使用授权服务器的公钥(通常通过 JWKS 端点获取)验证 JWT 签名和声明。

检查作用域

确保令牌包含所请求操作所需的作用域。

公布支持

可以选择在 server/discover 响应中包含该扩展(建议这样做,以便发现):

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    // Other fields...
    "capabilities": {
      "extensions": {
        "io.modelcontextprotocol/oauth-client-credentials": {},
      },
    },
  },
}

SDK 示例

官方 MCP SDK 内置客户端凭据身份验证支持,并会自动处理令牌获取和刷新。

安装 SDK

TypeScript

npm install @modelcontextprotocol/client

Python

pip install mcp

创建提供方并连接

请选择与你的设置匹配的凭据格式:

使用客户端密钥

TypeScript

import {
  Client,
  ClientCredentialsProvider,
  StreamableHTTPClientTransport,
} from "@modelcontextprotocol/client";

const provider = new ClientCredentialsProvider({
  clientId: "my-service",
  clientSecret: "s3cr3t",
});

const client = new Client(
  { name: "my-service", version: "1.0.0" },
  { capabilities: {} },
);

const transport = new StreamableHTTPClientTransport(
  new URL("https://mcp.example.com/mcp"),
  { authProvider: provider },
);

await client.connect(transport);

// Use the client
const tools = await client.listTools();
console.log(
  "Available tools:",
  tools.tools.map((t) => t.name),
);

await transport.close();

Python

import asyncio

import httpx2

from mcp import Client
from mcp.client.auth.extensions.client_credentials import (
    ClientCredentialsOAuthProvider,
)
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken


class InMemoryTokenStorage:
    def __init__(self) -> None:
        self.tokens: OAuthToken | None = None
        self.client_info: OAuthClientInformationFull | None = None

    async def get_tokens(self) -> OAuthToken | None:
        return self.tokens

    async def set_tokens(self, tokens: OAuthToken) -> None:
        self.tokens = tokens

    async def get_client_info(self) -> OAuthClientInformationFull | None:
        return self.client_info

    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
        self.client_info = client_info


provider = ClientCredentialsOAuthProvider(
    server_url="https://mcp.example.com/mcp",
    storage=InMemoryTokenStorage(),
    client_id="my-service",
    client_secret="s3cr3t",
    scopes="read write",
)


async def main() -> None:
    async with httpx2.AsyncClient(auth=provider) as http_client:
        transport = streamable_http_client(
            "https://mcp.example.com/mcp",
            http_client=http_client,
        )
        async with Client(transport) as client:
            # Use the client
            tools = await client.list_tools()
            print("Available tools:", [t.name for t in tools.tools])


if __name__ == "__main__":
    asyncio.run(main())

使用 JWT 私钥

TypeScript

import {
  Client,
  PrivateKeyJwtProvider,
  StreamableHTTPClientTransport,
} from "@modelcontextprotocol/client";

const provider = new PrivateKeyJwtProvider({
  clientId: "my-service",
  privateKey: process.env.CLIENT_PRIVATE_KEY_PEM,
  algorithm: "RS256",
});

const client = new Client(
  { name: "my-service", version: "1.0.0" },
  { capabilities: {} },
);

const transport = new StreamableHTTPClientTransport(
  new URL("https://mcp.example.com/mcp"),
  { authProvider: provider },
);

await client.connect(transport);

// Use the client
const tools = await client.listTools();
console.log(
  "Available tools:",
  tools.tools.map((t) => t.name),
);

await transport.close();

Python

import asyncio
from pathlib import Path

import httpx2

from mcp import Client
from mcp.client.auth.extensions.client_credentials import (
    PrivateKeyJWTOAuthProvider,
    SignedJWTParameters,
)
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken


class InMemoryTokenStorage:
    def __init__(self) -> None:
        self.tokens: OAuthToken | None = None
        self.client_info: OAuthClientInformationFull | None = None

    async def get_tokens(self) -> OAuthToken | None:
        return self.tokens

    async def set_tokens(self, tokens: OAuthToken) -> None:
        self.tokens = tokens

    async def get_client_info(self) -> OAuthClientInformationFull | None:
        return self.client_info

    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
        self.client_info = client_info


# Create a signed JWT assertion provider from key parameters
jwt_params = SignedJWTParameters(
    issuer="my-service",
    subject="my-service",
    signing_key=Path("private_key.pem").read_text(),
    signing_algorithm="RS256",
    lifetime_seconds=300,
)

provider = PrivateKeyJWTOAuthProvider(
    server_url="https://mcp.example.com/mcp",
    storage=InMemoryTokenStorage(),
    client_id="my-service",
    assertion_provider=jwt_params.create_assertion_provider(),
    scopes="read write",
)


async def main() -> None:
    async with httpx2.AsyncClient(auth=provider) as http_client:
        transport = streamable_http_client(
            "https://mcp.example.com/mcp",
            http_client=http_client,
        )
        async with Client(transport) as client:
            # Use the client
            tools = await client.list_tools()
            print("Available tools:", [t.name for t in tools.tools])


if __name__ == "__main__":
    asyncio.run(main())

客户端支持

不同客户端对该扩展的支持情况各不相同。扩展需要选择启用,绝不会默认激活。

请查看客户端矩阵,了解各 MCP 客户端当前的实现状态。

相关资源

ext-auth 仓库

源代码和参考实现

完整规范

包含规范性要求的技术规范

RFC 6749:客户端凭据许可

底层 OAuth 2.0 规范

RFC 7523:JWT Bearer 断言

JWT 断言格式规范