本篇是《MCP 开发实战教程》专栏的第 7 篇。上一篇我们实现了数据源的安全连接,但认证部分用的是最简单的方案------环境变量里的静态密码。本篇将带你理解 MCP 的正式认证体系:OAuth 2.1、AS/RS 分离、PKCE、动态客户端注册------让你的 MCP Server 支持企业级的 SSO 集成。
引言
你可能有过这种体验:你的 MCP Server 在团队内部用得好好的,有一天领导说"能不能让客户也用?"你心想,加个 API Key 不就行了?然后安全团队找到你:"不行,要走 OAuth。"你开始查 OAuth 文档,发现 OAuth 2.0 有四种授权模式,OAuth 2.1 又改了一堆东西,MCP 规范还要求什么 RFC 9728、RFC 8707......越看越懵。
上一篇我们用环境变量管理数据库密码,这在内部使用时没问题。但当你的 MCP Server 需要:
- 服务多个不同的客户端(Claude Desktop、Cursor、自研 Agent)
- 代表不同用户执行操作
- 与企业 SSO 系统集成
- 满足合规要求(审计谁在什么时候访问了什么)
你就需要一个正式的认证和授权体系。MCP 规范选择了 OAuth 2.1 作为标准。
本篇会带你搞清楚四件事:为什么选 OAuth 2.1、AS/RS 分离是什么意思、MCP 的 OAuth 流程长什么样、以及怎么用 FastMCP 实现它。
读完本篇,你将获得:
- 理解 MCP OAuth 2.1 授权架构的设计动机
- 掌握 AS/RS 分离、RFC 9728、RFC 8707 的核心概念
- 获得一个可运行的 OAuth 保护 MCP Server 实现
1. 为什么是 OAuth 2.1
1.1 MCP 的认证需求
MCP Server 的认证需求与传统 Web API 不同:
| 特点 | 说明 |
|---|---|
| 多客户端 | 同一个 Server 要服务 Claude Desktop、Cursor、自研 Agent 等多个客户端 |
| 多用户 | 每个客户端代表不同的用户执行操作 |
| 动态发现 | 客户端需要自动发现 Server 支持的认证方式 |
| 桌面应用 | 很多 MCP 客户端是桌面应用,不是 Web 应用 |
OAuth 2.1 天然解决了这些问题------它就是为"多客户端、多用户、第三方授权"设计的。
1.2 OAuth 2.1 vs OAuth 2.0
OAuth 2.1 是 OAuth 2.0 的安全加固版,主要变化:
| 变化 | OAuth 2.0 | OAuth 2.1 |
|---|---|---|
| PKCE | 可选(仅公开客户端) | 强制所有客户端 |
| 隐式授权 | 支持 | 完全移除 |
| 密码授权 | 支持 | 完全移除 |
| 重定向 URI | 模糊匹配 | 精确匹配 |
| 刷新令牌 | 无特殊要求 | 一次性使用或绑定客户端 |
这些变化对 MCP 来说很重要:
- PKCE 强制 → 防止授权码被截获(桌面应用的常见风险)
- 隐式授权移除 → 不再有 Token 出现在 URL 中的安全隐患
- 精确重定向 → 防止开放重定向攻击
1.3 角色定义
OAuth 2.1 定义了四个角色,在 MCP 中的对应:
| OAuth 角色 | MCP 中的对应 | 说明 |
|---|---|---|
| Resource Owner(资源所有者) | 用户 | 拥有数据的人 |
| Client(客户端) | MCP Client | 代表用户请求访问 |
| Authorization Server(授权服务器, AS) | 外部 IdP | 负责认证用户、颁发令牌 |
| Resource Server(资源服务器, RS) | MCP Server | 持有资源,验证令牌 |
关键设计:AS 和 RS 是分离的 。MCP Server(RS)不自己做认证,而是委托给专门的授权服务器(AS)。这就是所谓的 AS/RS 分离。
2. AS/RS 分离:为什么要分开
2.1 传统方式的问题
很多开发者第一次实现认证时,会把认证逻辑直接放在 MCP Server 里:
python
# 不推荐:Server 自己做认证
@mcp.tool()
def get_data(token: str) -> str:
"""获取数据"""
user = verify_token(token) # Server 自己验证 Token
if not user:
return "认证失败"
return query_data(user)
这种方式的问题:
| 问题 | 说明 |
|---|---|
| 无法支持 SSO | 每个 Server 自己管认证,用户要登录多次 |
| Token 管理复杂 | Server 要自己处理 Token 生成、刷新、撤销 |
| 安全责任不清 | 认证逻辑分散在各个 Server 中,难以审计 |
| 扩展困难 | 新增 Server 要重新实现认证 |
2.2 AS/RS 分离的架构
AS/RS 分离后,职责清晰:
arduino
用户
│
├─→ Client ──→ AS(授权服务器)── 颁发 Token
│ │
│ ↓
└─→ Client ──→ RS(MCP Server)── 验证 Token ──→ 数据
| 组件 | 职责 |
|---|---|
| AS | 认证用户、颁发 Token、管理客户端注册、支持 SSO |
| RS(MCP Server) | 验证 Token、提供数据、不关心用户怎么登录 |
| Client | 引导用户到 AS 登录、获取 Token、用 Token 访问 RS |
MCP Server 只需要做两件事:
- 告诉 Client "我的 AS 在哪里"(通过 RFC 9728)
- 验证 Client 带来的 Token 是否有效
认证的复杂性全部由 AS 承担。你可以用现有的 IdP 服务(Keycloak、Auth0、WorkOS、Okta、Azure AD),不需要自己实现。
2.3 AS/RS 分离的好处
| 好处 | 说明 |
|---|---|
| SSO 集成 | 用户登录一次,所有 MCP Server 都能用 |
| 集中管理 | 认证策略、用户管理、审计日志都在 AS 中 |
| 安全专业性 | IdP 服务商有专业的安全团队维护 |
| 合规友好 | 审计时只需要检查 AS,不需要检查每个 Server |
3. MCP OAuth 流程:五步走
MCP 的 OAuth 流程有五个阶段。前两个阶段每个 Client-Server 对只执行一次,后三个阶段每次获取新 Token 都会执行。
3.1 第一步:Server 元数据发现
Client 首先需要知道 MCP Server 的 AS 在哪里。它向 Server 的 well-known 端点发送请求:
ruby
GET https://mcp-server.example.com/.well-known/oauth-protected-resource
Server 返回(RFC 9728 Protected Resource Metadata):
json
{
"resource": "https://mcp-server.example.com",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["mcp:read", "mcp:tools", "mcp:prompts"],
"bearer_methods_supported": ["header"]
}
关键字段:
resource:这个 MCP Server 的标识authorization_servers:AS 的地址scopes_supported:支持的权限范围
然后 Client 向 AS 发现元数据:
sql
GET https://auth.example.com/.well-known/oauth-authorization-server
AS 返回(RFC 8414 Authorization Server Metadata):
json
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"registration_endpoint": "https://auth.example.com/register",
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"]
}
这个"发现三部曲"(RFC 9728 + RFC 8414 + RFC 7591)让 Client 可以自动发现认证流程需要的所有端点,不需要任何硬编码配置。

3.2 第二步:动态客户端注册
Client 需要在 AS 注册自己,获取 client_id。MCP 规范支持 Dynamic Client Registration(RFC 7591)------Client 可以在运行时自动注册,不需要提前手动配置。
bash
POST https://auth.example.com/register
Content-Type: application/json
{
"client_name": "Claude Desktop",
"redirect_uris": ["http://localhost:9090/callback"],
"grant_types": ["authorization_code"],
"scope": "mcp:read mcp:tools",
"token_endpoint_auth_method": "none"
}
AS 返回:
json
{
"client_id": "abc123",
"client_name": "Claude Desktop",
"redirect_uris": ["http://localhost:9090/callback"],
"grant_types": ["authorization_code"]
}
安全注意 :动态注册意味着任何 Client 都可以注册。对于内部 Server,你可能需要限制注册来源或要求预共享注册令牌。MCP 规范引入了 Client ID Metadata Documents(CIMD) 作为 DCR 的替代方案,提供更好的安全性。
3.3 第三步:授权请求(带 PKCE)
Client 生成 PKCE 参数,引导用户到 AS 登录:
python
import hashlib
import base64
import secrets
# 生成 PKCE 参数
code_verifier = secrets.token_urlsafe(64) # 43-128 字符的随机字符串
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
Client 构造授权 URL,引导用户浏览器打开:
ini
https://auth.example.com/authorize
?response_type=code
&client_id=abc123
&redirect_uri=http://localhost:9090/callback
&scope=mcp:read mcp:tools
&state=xyz789
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&resource=https://mcp-server.example.com
注意 resource 参数(RFC 8707)------它告诉 AS 这个 Token 是为哪个 MCP Server 颁发的。AS 会将这个信息绑定到 Token 中,RS 验证时可以检查 Token 的目标是否是自己。
用户在 AS 页面登录并授权后,AS 重定向回 Client:
bash
http://localhost:9090/callback?code=auth_code_here&state=xyz789
3.4 第四步:Token 请求
Client 用授权码换取 Access Token:
ini
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=auth_code_here
&redirect_uri=http://localhost:9090/callback
&client_id=abc123
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&resource=https://mcp-server.example.com
AS 验证 PKCE 和授权码后,返回 Token:
json
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"scope": "mcp:read mcp:tools"
}
3.5 第五步:资源访问
Client 用 Access Token 访问 MCP Server:
bash
POST https://mcp-server.example.com/mcp
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
MCP Server 验证 Token:
- 检查 Token 签名是否有效
- 检查 Token 是否过期
- 检查 Token 的
aud(audience)是否是自己(RFC 8707 绑定) - 检查 Token 的
scope是否允许该操作
验证通过后,Server 处理请求并返回结果。
4. 实战:用 FastMCP 实现 OAuth 保护
4.1 FastMCP 内置 OAuth 支持
FastMCP 提供了开箱即用的 JWT Token 验证支持(需要 FastMCP v2.11.0+)。最简单的方式是使用 JWTVerifier:
python
"""OAuth 保护的 MCP Server"""
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier
# 配置 JWT 验证器
verifier = JWTVerifier(
jwks_uri=os.environ["JWKS_URI"], # AS 的 JWKS 端点
issuer=os.environ["TOKEN_ISSUER"], # Token 颁发者
audience=os.environ.get("TOKEN_AUDIENCE"), # Token 受众(可选)
)
mcp = FastMCP("安全数据服务", auth=verifier)
@mcp.tool()
def search_users(keyword: str) -> str:
"""搜索用户"""
# Token 验证由中间件自动完成
# 这里只处理业务逻辑
results = db.search_users(keyword)
return format_results(results)
@mcp.tool()
def get_user_detail(user_id: int) -> str:
"""获取用户详情"""
user = db.get_user(user_id)
if not user:
return f"未找到用户 #{user_id}"
return format_user(user)
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
JWTVerifier 自动完成:
- 从 JWKS 端点获取 AS 的公钥
- 验证 Token 签名(支持 RS256、ES256 等算法)
- 检查 Token 是否过期
- 验证
issuer和audience
注意 :
JWTVerifier不直接支持required_scopes参数。如果需要基于 scope 的权限控制,可以在工具层面检查 Token 中的scope字段。
4.2 多 Provider 集成
FastMCP 的 JWTVerifier 是通用的------任何支持 JWKS 的 IdP 都可以直接使用。此外,部分 Provider 有专门的集成模块:
| Provider | 集成方式 |
|---|---|
| Auth0 | JWTVerifier + Auth0 JWKS URI |
| WorkOS | 专门的 DCR 集成(RemoteAuthProvider) |
| Descope | 专门的集成模块 |
| Keycloak | JWTVerifier + Keycloak JWKS URI |
| Azure AD / Entra ID | JWTVerifier + Azure JWKS URI |
| AWS Cognito | JWTVerifier + Cognito JWKS URI |
以 Auth0 为例:
python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier
verifier = JWTVerifier(
jwks_uri=f"https://{os.environ['AUTH0_DOMAIN']}/.well-known/jwks.json",
issuer=f"https://{os.environ['AUTH0_DOMAIN']}/",
audience=os.environ["AUTH0_AUDIENCE"],
)
mcp = FastMCP("Auth0 保护的服务器", auth=verifier)
以 Keycloak 为例:
python
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier
verifier = JWTVerifier(
jwks_uri=f"{os.environ['KEYCLOAK_URL']}/realms/{os.environ['KEYCLOAK_REALM']}/protocol/openid-connect/certs",
issuer=f"{os.environ['KEYCLOAK_URL']}/realms/{os.environ['KEYCLOAK_REALM']}",
audience=os.environ.get("KEYCLOAK_CLIENT_ID"),
)
mcp = FastMCP("Keycloak 保护的服务器", auth=verifier)
4.3 手动 Token 验证
如果你需要更细粒度的控制,可以手动验证 Token:
python
import jwt
import httpx
from functools import lru_cache
@lru_cache(maxsize=1)
def get_jwks(jwks_uri: str) -> dict:
"""获取并缓存 JWKS"""
response = httpx.get(jwks_uri)
return response.json()
def verify_token(token: str, jwks_uri: str, issuer: str, audience: str) -> dict | None:
"""手动验证 JWT Token"""
try:
# 获取 JWKS
jwks = get_jwks(jwks_uri)
# 解码 Token header 获取 kid
unverified = jwt.get_unverified_header(token)
kid = unverified.get("kid")
# 查找对应的公钥
key = None
for jwk in jwks.get("keys", []):
if jwk.get("kid") == kid:
key = jwt.algorithms.RSAAlgorithm.from_jwk(jwk)
break
if not key:
return None
# 验证 Token
payload = jwt.decode(
token,
key,
algorithms=["RS256"],
issuer=issuer,
audience=audience,
)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
5. 深入理解:关键 RFC 的作用
5.1 RFC 9728:Protected Resource Metadata
RFC 9728 定义了 RS(MCP Server)如何告诉 Client "我的 AS 在哪里"。
MCP Server 在 /.well-known/oauth-protected-resource 端点返回元数据,Client 据此知道去哪里认证。这解决了"鸡生蛋"问题------Client 在认证之前就需要知道 AS 的地址。
5.2 RFC 8707:Resource Indicators
RFC 8707 解决了"Token 绑定"问题。Client 在授权请求和 Token 请求中都包含 resource 参数,AS 将 Token 绑定到特定的 RS。
这防止了混淆代理人攻击(Confused Deputy Attack):
arduino
场景:用户授权了 GitHub MCP Server 的 Token
攻击:恶意 Server 试图用这个 Token 访问 GitHub API
没有 RFC 8707:
Token → 任何 Server 都能用 → 攻击成功
有 RFC 8707:
Token(绑定 github.com)→ 恶意 Server 验证 audience 不匹配 → 拒绝
当前状态 :
resource参数的支持取决于 AS 的实现。MCP 规范要求 Client 必须发送resource,但 AS 可以选择忽略它。MCP Steering Committee 正在推动主流 IdP(Okta、Entra ID、AWS)支持这个参数。
5.3 PKCE:防授权码截获
PKCE(Proof Key for Code Exchange)防止授权码在传输过程中被截获。即使攻击者获取了授权码,没有 code_verifier 也无法换取 Token。
这对 MCP 桌面客户端特别重要------桌面应用的本地回调端口可能被其他应用监听。PKCE 确保只有发起授权请求的 Client 才能完成 Token 交换。
6. 常见问题与踩坑记录
Q1: 内部使用的 MCP Server 也需要 OAuth 吗?
不一定。如果你的 Server 只在本地运行(STDIO 传输),不需要 OAuth------STDIO 的安全性来自进程隔离。但如果你的 Server 通过 HTTP 暴露(即使是内网),建议加上 OAuth,因为 HTTP 端口可能被意外暴露。
Q2: 动态客户端注册安全吗?
有风险。默认情况下,任何 Client 都可以注册。建议:
- 对内部 Server:限制注册来源或要求预共享注册令牌
- 对公开 Server:使用 CIMD(Client ID Metadata Documents)替代传统 DCR
- 对企业环境:使用管理员预注册客户端
Q3: 我可以用 API Key 代替 OAuth 吗?
可以,但功能有限。API Key 只能标识"哪个应用在调用",不能标识"代表哪个用户"。如果你的 Server 需要区分不同用户的数据访问权限,必须用 OAuth。
Q4: Token 过期后怎么办?
Client 使用 refresh_token 获取新的 access_token,不需要用户重新登录。MCP 的 OAuth 流程中,Token 刷新对用户是透明的。
Q5: 如何选择 IdP?
| 场景 | 推荐 IdP |
|---|---|
| 企业内部 | Keycloak(开源)、Azure AD、Okta |
| SaaS 产品 | Auth0、WorkOS、Stytch |
| AWS 生态 | Cognito |
| 快速原型 | WorkOS(免费层慷慨)或 Keycloak |
总结
-
OAuth 2.1 是 MCP 的认证标准:它解决了 MCP 的多客户端、多用户、动态发现需求。PKCE 强制、隐式授权移除、精确重定向等安全加固让桌面应用场景更安全。
-
AS/RS 分离是核心设计:MCP Server(RS)不自己做认证,而是委托给专门的授权服务器(AS)。这让 SSO 集成、集中管理、合规审计都成为可能。
-
五步流程自动完成:Server 元数据发现 → 动态客户端注册 → 授权请求(带 PKCE)→ Token 请求 → 资源访问。Client 和 Server 通过 well-known 端点自动发现对方的能力。
-
RFC 9728 和 RFC 8707 是关键:RFC 9728 让 Client 发现 Server 的 AS,RFC 8707 让 Token 绑定到特定 Server。两者共同防止了混淆代理人攻击。
-
FastMCP 让实现变简单 :
TokenVerifier中间件自动处理 Token 验证,支持 Auth0、WorkOS、Keycloak 等多种 Provider。几行代码就能给 MCP Server 加上企业级认证。
下篇预告
认证解决了"你是谁"的问题,下一篇解决"怎么高效访问"的问题。《MCP 网关》将带你了解 Cloudflare MCP Server Portals、IBM/Azure 等企业级网关方案------让 MCP Server 的部署、扩展、监控变得简单。
附录:参考资料
- MCP Authorization Specification
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 8707: Resource Indicators for OAuth 2.0
- RFC 7591: OAuth 2.0 Dynamic Client Registration
- RFC 7636: PKCE (Proof Key for Code Exchange)
- MCP OAuth: How OAuth 2.1 Works in MCP
- Resource Indicators in OAuth 2.0 (WorkOS)
- Update To MCP Authorization Spec - Resource Parameter
- FastMCP Authentication Documentation
- Protecting MCP Server with OAuth 2.1 and Keycloak