Skip to content

身份验证

验证你的用户

许多Apps SDK应用可以匿名运行,但用户特定或写入操作需要身份验证。你可以与现有的授权服务器集成,或使用每个工具的安全配置来混合匿名和经过身份验证的端点。

使用OAuth 2.1的自定义身份验证

组件

  • 资源服务器 – 暴露工具并验证访问Token的MCP服务器
  • 授权服务器 – 颁发Token的身份提供者
  • 客户端 – 代表用户行事的ChatGPT

必需端点

你的授权服务器必须提供:

  • /.well-known/oauth-protected-resource
  • /.well-known/openid-configuration
  • token_endpoint
  • registration_endpoint

实践中的流程

  1. ChatGPT查询MCP服务器以获取资源元数据
  2. ChatGPT向授权服务器注册
  3. 用户进行身份验证并同意作用域
  4. ChatGPT将授权代码交换为访问Token
  5. 服务器在每个请求上验证Token

代码示例(Python)

python
from mcp.server.fastmcp import FastMCP
from mcp.server.auth.settings import AuthSettings
from mcp.server.auth.provider import TokenVerifier, AccessToken

class MyVerifier(TokenVerifier):
    async def verify_token(self, token: str) -> AccessToken | None:
        payload = validate_jwt(token, jwks_url)
        if "user" not in payload.get("permissions", []):
            return None
        return AccessToken(
            token=token,
            client_id=payload["azp"],
            subject=payload["sub"],
            scopes=payload.get("permissions", []),
            claims=payload,
        )

mcp = FastMCP(
    name="kanban-mcp",
    stateless_http=True,
    token_verifier=MyVerifier(),
    auth=AuthSettings(
        issuer_url="https://your-tenant.us.auth0.com",
        resource_server_url="https://example.com/mcp",
        required_scopes=["user"],
    ),
)

validate_jwt 是一个辅助函数,它使用来自授权服务器JWKS端点的公钥验证Token签名和过期时间。如果Token有效,验证器会返回一个 AccessToken 对象,MCP服务器会将其附加到工具上下文中,以便你的工具实现可以访问用户的身份和作用域。

代码示例(TypeScript)

typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { OAuth2Settings, TokenVerifier } from "@modelcontextprotocol/sdk/server/auth.js";

const verifier: TokenVerifier = async (token: string) => {
  const payload = await validateJwt(token, jwksUrl);
  if (!payload.permissions.includes("user")) {
    return null;
  }
  return {
    token,
    clientId: payload.azp,
    subject: payload.sub,
    scopes: payload.permissions,
    claims: payload,
  };
};

const server = new McpServer({
  name: "kanban-server",
  version: "1.0.0",
  auth: {
    oauth2: {
      issuerUrl: "https://your-tenant.us.auth0.com",
      resourceServerUrl: "https://example.com/mcp",
      requiredScopes: ["user"],
    },
  },
  tokenVerifier: verifier,
});

使用 securitySchemes 的每个工具身份验证

可以使用两种方案类型定义每个工具的身份验证:

  • "noauth" – 可以匿名调用
  • "oauth2" – 需要OAuth 2.0

TypeScript SDK示例

公共搜索工具:

typescript
server.registerTool(
  "search",
  {
    title: "搜索文档",
    securitySchemes: [
      { type: "noauth" },
      { type: "oauth2", scopes: ["search.read"] },
    ],
    inputSchema: { query: z.string() },
  },
  async ({ query }, context) => {
    // 如果用户已通过身份验证,上下文包含accessToken
    const results = await searchDocs(query, context.accessToken);
    return {
      content: [{ type: "text", text: `找到 ${results.length} 个结果` }],
      structuredContent: { results },
    };
  }
);

创建文档工具(需要身份验证):

typescript
server.registerTool(
  "create_doc",
  {
    title: "创建文档",
    securitySchemes: [{ type: "oauth2", scopes: ["docs.write"] }],
    inputSchema: { title: z.string(), content: z.string() },
  },
  async ({ title, content }, context) => {
    if (!context.accessToken) {
      throw new Error("需要身份验证");
    }
    const doc = await createDoc(title, content, context.accessToken);
    return {
      content: [{ type: "text", text: `已创建文档: ${doc.id}` }],
      structuredContent: { doc },
    };
  }
);

Python SDK示例

python
@mcp.tool(
    title="搜索文档",
    security_schemes=[
        {"type": "noauth"},
        {"type": "oauth2", "scopes": ["search.read"]},
    ],
)
async def search(query: str, context: ToolContext) -> ToolResponse:
    results = await search_docs(query, context.access_token)
    return ToolResponse(
        content=[{"type": "text", "text": f"找到 {len(results)} 个结果"}],
        structured_content={"results": results},
    )

@mcp.tool(
    title="创建文档",
    security_schemes=[{"type": "oauth2", "scopes": ["docs.write"]}],
)
async def create_doc(title: str, content: str, context: ToolContext) -> ToolResponse:
    if not context.access_token:
        raise ValueError("需要身份验证")
    doc = await create_doc_api(title, content, context.access_token)
    return ToolResponse(
        content=[{"type": "text", "text": f"已创建文档: {doc['id']}"}],
        structured_content={"doc": doc},
    )

测试和推出建议

  • 从开发租户开始
  • 在广泛推出之前使用受信任的测试者
  • 计划Token轮换和撤销

开发租户

大多数身份提供者允许你创建单独的开发和生产租户。在开发租户中测试你的OAuth流程,以避免影响生产用户。

受信任的测试者

在公开推出之前,邀请一小组用户测试你的身份验证流程。这有助于你在更广泛的受众看到之前捕获边缘情况和可用性问题。

Token轮换和撤销

实现Token刷新,以便用户在Token过期时不必重新进行身份验证。还提供一种让用户撤销访问的方法,这对于安全和合规性很重要。

最佳实践

最小化作用域请求

仅请求你的应用实际需要的作用域。这减少了用户同意摩擦并提高了安全性。

使用短期Token

将访问Token寿命保持在短期(例如15分钟),并使用刷新Token进行长期访问。这限制了Token泄露的影响。

记录访问

记录所有身份验证的操作,以便你可以审计访问并调试问题。包括用户ID、操作和时间戳。

处理Token过期

优雅地处理Token过期,方法是捕获401响应并提示用户重新进行身份验证或自动刷新Token。

支持Token撤销

实现Token撤销端点,以便用户可以撤销访问,你的服务器可以使Token无效。

故障排除

无效Token错误

如果你看到"无效Token"错误:

  • 验证Token签名是否与授权服务器的JWKS端点匹配
  • 检查Token是否已过期
  • 确保Token包含所需的作用域

授权循环

如果用户陷入授权循环:

  • 检查你的 redirect_uri 是否与注册的URI完全匹配
  • 验证你的授权服务器是否正确实现了PKCE
  • 确保你的服务器正确处理授权代码交换

CORS错误

如果你在开发中看到CORS错误:

  • 确保你的授权服务器允许来自你的开发域的跨源请求
  • 添加 Access-Control-Allow-Origin 头部到你的响应
  • 对于生产环境,使用你的生产域配置CORS

后续步骤

通过身份验证,你可以构建需要用户身份和权限的工具。浏览存储页面,了解如何持久化用户数据,或查看示例页面以获取完整的端到端演示。