Cloudflare 为 OAuth 增加了“可选 scope”能力:客户端开发者可以标记哪些权限允许用户在授权页面主动取消。这个变化针对的是一个越来越常见的问题,尤其是在 MCP server 和 AI agent 场景中:Agent 可能需要请求它潜在会使用的全部权限,但用户并不一定愿意一次性授予所有能力。
从“全部授权”到“按能力选择”
传统 OAuth 授权通常把 scope 视为一个整体。客户端在授权请求中列出 read:documents、write:documents、delete:documents 等权限,用户要么全部同意,要么拒绝整个请求。
部分同意并不是新概念,很多 OAuth 实现已经允许用户取消某些权限。但 Cloudflare 这次强调的是另一层控制:由客户端开发者明确声明哪些 scope 可以被取消,哪些 scope 必须保留。
这能解决两个实际问题:
- 核心能力可以设为必选,避免客户端拿到一个无法正常工作的授权结果。
- 高风险或低频能力可以设为可选,让用户根据实际信任程度决定是否授予。
例如,一个文档 Agent 可能需要读取文档才能回答问题,但只有在用户要求“帮我修改文档”时才需要写权限。删除权限则可能永远不应该默认开启。
为什么 MCP server 更需要这项能力
MCP server 通常会向 Agent 暴露一组工具。Agent 在初始化或连接阶段,可能需要请求这些工具未来可能使用的权限集合,而不是只请求当前对话立即需要的权限。
这会形成典型的权限膨胀:
Agent 可能调用:
- search_documents
- read_document
- update_document
- delete_document
实际用户问题:
“帮我找出上个月的设计文档。”
如果授权流程只能整体批准,用户可能被迫同时授予搜索、读取、更新和删除权限。可选 scope 允许服务端把权限表达成更接近实际风险的结构:
必选:documents:read
可选:documents:write
可选:documents:delete
用户仍然可以拒绝整个客户端,但也可以保留只读能力,关闭写入和删除能力。对于连接第三方 SaaS 的 Agent,这种粒度比一个笼统的“访问你的账户”更容易解释,也更容易获得用户信任。
一个可改造的授权请求示例
下面的命令是一个通用 OAuth 示例。实际接入时,需要把授权端点、客户端 ID、重定向地址和 scope 名称替换成目标服务的配置。optional_scopes 的具体编码方式取决于 OAuth 服务商的协议;如果服务商提供专用参数,应优先使用其官方格式。
curl -G 'https://auth.example.com/oauth/authorize' \
--data-urlencode 'response_type=code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'redirect_uri=https://app.example.com/oauth/callback' \
--data-urlencode 'scope=documents:read documents:write documents:delete' \
--data-urlencode 'optional_scope=documents:write documents:delete' \
--data-urlencode 'state=RANDOM_CSRF_VALUE'
这个请求表达了三件事:
documents:read是客户端运行所需的核心权限。documents:write和documents:delete可以由用户取消。state仍然必须是不可预测且与用户会话绑定的值,用来防止 OAuth 回调被 CSRF 攻击利用。
如果平台的授权页面返回最终同意的 scope,服务端应当把它当作真实权限边界,而不是假设所有请求的 scope 都已获得。
一个最小的 Python 处理逻辑可以这样写:
from urllib.parse import parse_qs
REQUIRED_SCOPES = {"documents:read"}
OPTIONAL_SCOPES = {"documents:write", "documents:delete"}
def validate_granted_scopes(scope_value: str) -> set[str]:
granted = set(scope_value.split())
unknown = granted - REQUIRED_SCOPES - OPTIONAL_SCOPES
if unknown:
raise ValueError(f"Unexpected scopes: {sorted(unknown)}")
missing = REQUIRED_SCOPES - granted
if missing:
raise PermissionError(f"Required scopes were not granted: {sorted(missing)}")
return granted
def capabilities_for_token(token_response: dict) -> dict[str, bool]:
scopes = validate_granted_scopes(token_response.get("scope", ""))
return {
"can_read": "documents:read" in scopes,
"can_write": "documents:write" in scopes,
"can_delete": "documents:delete" in scopes,
}
if __name__ == "__main__":
token = {
"scope": "documents:read documents:write"
}
print(capabilities_for_token(token))
运行后,应用可以根据实际 scope 关闭写入和删除工具,而不是等到工具调用失败后才返回错误。对于 MCP server,可以把这些能力映射到工具注册或调用前检查中:没有 documents:delete 就不要向 Agent 暴露删除工具,或者在调用时明确返回权限不足。
开发者需要重新设计什么
可选 scope 不是把权限列表加一个标记就结束了。客户端还需要让每种授权结果都能正常工作。
工具和界面要适配部分授权。 如果用户只授予读取权限,应用仍应提供搜索和查看功能,而不是显示一个模糊的“连接失败”。
高风险能力应保持可选。 写入、删除、发送消息、执行付款等权限通常比读取权限更敏感。把它们设置为可取消,可以降低一次性授权过宽的问题。
服务端必须以 token 返回的 scope 为准。 不能仅依据初始请求中的 scope 决定能力。OAuth 提供方可能返回一个被用户删减后的 scope 集合,资源服务器也可能在后续刷新流程中改变授权状态。
权限名称要能被用户理解。 documents:write 比 access_level_7 更容易解释,但授权页面还需要进一步显示“修改文档”“删除文档”等面向用户的描述。对于 Agent,权限说明应直接对应它可以执行的动作。
落地检查清单
采用可选 scope 时,可以检查以下事项:
- 把最小可运行能力定义为必选 scope。
- 将写入、删除和外部副作用操作拆成独立的可选 scope。
- 在回调和 token 处理逻辑中校验实际授予的 scope。
- 根据最终权限动态注册或禁用 MCP 工具。
- 为“只读授权”“读写授权”和“拒绝必选权限”分别编写测试。
- 在授权页面清楚解释用户取消每个可选权限后的影响。
- 继续校验
state、重定向地址、PKCE 和 token 存储安全性。
Cloudflare 的变化把 OAuth 的“部分同意”推进了一步:权限是否可以被取消,不再完全由授权页面自行决定,而是成为客户端能力设计的一部分。对于 MCP server 和 Agent,这意味着开发者可以把“它可能做什么”拆成更细的授权边界,让用户保留核心功能,同时拒绝不必要的高风险操作。