跳到主要内容

05 · 认证:让某些工具要登录才能用

这章讲 authenticated_server_python:它有两个工具,一个免登录、一个必须 OAuth。读完你能说清 Apps SDK 的认证三件事:声明、发现、挑战。

1. 要解决的小问题

「搜披萨店」谁都能用,但「看我的历史订单」是私人数据,得先登录。需要一种办法让 ChatGPT 知道:哪些工具要登录、去哪登录、没登录时怎么提示去登录

2. 三件事概览

阶段谁做怎么做
① 声明server 列工具时每个工具带 securitySchemes:noauth 还是 oauth2
② 发现server 暴露元数据RFC 9728 的 /.well-known/oauth-protected-resource,指向授权服务器
③ 挑战server 在 call_tool 里没带有效 token 就回 WWW-Authenticate,ChatGPT 据此发起登录

3. 声明:securitySchemes

两套 scheme 定义在 authenticated_server_python/main.py:187-200:

  • MIXED_TOOL_SECURITY_SCHEMES = [{noauth}, {oauth2}] —— 既可匿名也可登录(搜索工具用)。
  • OAUTH_ONLY_SECURITY_SCHEMES = [{oauth2}] —— 必须登录(历史订单工具用)。

列工具时两个工具各挂一套(authenticated_server_python/main.py:370-398)。注意 securitySchemes 既进 _meta(_tool_meta:343-344)又作为 types.Tool 的顶层字段传(:378、:392)。

4. 发现:RFC 9728 protected-resource 元数据

Server 注册一个标准路由 /.well-known/oauth-protected-resource{path}(authenticated_server_python/main.py:321-326protected_resource_metadata),返回 ProtectedResourceMetadata:

# 示意,非源码 —— 告诉客户端"去哪登录"
ProtectedResourceMetadata(
resource=RESOURCE_SERVER_URL, # 这资源是谁
authorization_servers=[AUTHORIZATION_SERVER_URL], # 去这个授权服务器拿 token
scopes_supported=RESOURCE_SCOPES,
)

URL 由 RESOURCE_SERVER_URL 解析拼出(authenticated_server_python/main.py:170-179);两个授权相关环境变量缺失会启动即报错(:151-164)——强制部署时配齐。

5. 挑战:WWW-Authenticate

关键在「需登录」工具的 call_tool 分支(authenticated_server_python/main.py:498-503):先从请求头抠 Bearer token,没有就返回 OAuth 挑战。

# 示意,非源码 —— 没 token 就发挑战
if not _get_bearer_token_from_request():
return _oauth_error_result("Authentication required: no access token provided.")

_oauth_error_result(:238-260)返回一个 isError=True 的结果,并在 _meta 里塞 mcp/www_authenticate 头(:253-257),值由 _build_www_authenticate_value(:228-235)拼出,里面带 resource_metadata="..." 指回 §4 那个元数据 URL。ChatGPT 收到这个挑战(inferred)就知道该把用户引到授权服务器登录,拿到 token 后重发请求。

抠 token 写得很防御(_get_bearer_token_from_request:263-318):依次尝试 request.headers、ASGI scope headers、dict 形式的 request,大小写都试,最后校验 Bearer 前缀。这是因为不同传输/中间件下 request 对象形态不一(inferred)。

6. 关键细节

  • 匿名工具的处理就没这道关卡:搜索工具(SEARCH_TOOL_NAME)的分支(:482-496)直接返回结果,不查 token。
  • 这个 server 同样复用了 03 章的 widgetSessionId(:352),以及 01 章的 _meta 四件套——认证只是叠加在同一套 widget 契约之上的一层。
  • 真正的 token 校验/换取用户身份不在本仓:server 只检查「有没有 Bearer token」,不验签、不查 scope(RESOURCE_SCOPES 是空列表 :168)。这是 demo 简化。

7. 代码地图

主题文件路径符号名
两套 schemeauthenticated_server_python/main.pyMIXED_TOOL_SECURITY_SCHEMESOAUTH_ONLY_SECURITY_SCHEMES
RFC 9728 元数据authenticated_server_python/main.pyprotected_resource_metadataPROTECTED_RESOURCE_METADATA
发挑战authenticated_server_python/main.py_oauth_error_result_build_www_authenticate_value
抠 tokenauthenticated_server_python/main.py_get_bearer_token_from_request
工具分支鉴权authenticated_server_python/main.py_call_tool_request(past_orders 分支)