成功路径端到端
B7 这一周是「给 MCP 工具网关装上 OAuth 2.1 资源服务器(RS)能力」的完整闭环。Day 61-64 建机理与 guard(RS 模型 / 三类攻击 / jose 校验内核 / 接进 toolRegistry),Day 65 走的是拒绝路径(401/403 + WWW-Authenticate),今天 Day 66 走的是成功路径——把一个只含最小 scope 的 token 注进
阶段: B7 · OAuth 2.1 + MCP 安全 + CI gate(Day 61-70) 标签: #oauth21 #least-privilege #audit-trail #mcp-security
今日导引(由浅入深)
B7 这一周是「给 MCP 工具网关装上 OAuth 2.1 资源服务器(RS)能力」的完整闭环。Day 61-64 建机理与 guard(RS 模型 / 三类攻击 / jose 校验内核 / 接进 toolRegistry),Day 65 走的是拒绝路径(401/403 + WWW-Authenticate),今天 Day 66 走的是成功路径——把一个只含最小 scope 的 token 注进 agent,让它经 server 成功调 2 个工具,并把 sub/aud/jti 写进审计行。在 B1→B18 能力曲线上,这是「安全可观测」支柱从「会拒」补齐到「会放且可追溯」的一格;明天 Day 67 起转向 eval CI gate(评测支柱)。今天的最小可判定产出:一份含 200 + 2 次工具调用 + 审计行 的成功 transcript(端到端实跑标注为待跑,需 key)。
由浅入深三层
- 浅:成功路径 = 让带 token 的请求返回 200。
- 中:200 不够——成功路径多一层要求:放行的同时留痕(sub/aud/jti 审计行),否则合规视角下和裸放行无异。
- 深:注入的 token 只含完成任务所需的最小 scope(scope 收敛),且每次调用把责任链钉进审计行,使「谁、用哪个 token、对哪个受众、调了什么」可重建。这是合规审计轨迹的基石。
1. 机理精读
最小权限(least privilege)在 token 层的落点 = scope 收敛。 OAuth 2.1 里 access token 的 scope 声明持有者「被授权做什么」。成功路径的关键不是「给一个能调所有工具的万能 token」,而是只注入完成当前任务所需的最小 scope 集合。这样即使 token 在会话中途泄露,攻击面也被限制在那几个 scope 上——与 Day 64「per-call scope 双校验」是同一最小权限原则在「签发面 vs 执行面」的两端:Day 64 管「调用时校验」,Day 66 管「签发时只给该给的」。
审计字段三元组 sub / aud / jti 是合规审计轨迹的基石。 一条可追溯的审计行要回答「谁、用哪个 token、对哪个受众、调了什么」:
sub(subject)= 主体身份,回答「谁」——是哪个 agent / 用户在调用;aud(audience)= 受众,回答「这个 token 是签给哪个 RS 用的」——配合 RFC 8707(Resource Indicators, 现行)阻止 token 被转去打另一个服务(confused deputy);jti(JWT ID)= token 唯一 id,回答「是哪一次签发的 token」——让同一主体的多次会话/多枚 token 可区分,也是 token 撤销列表(denylist)和重放检测的挂钩点。
为什么把这三者写进审计行而不是只记业务参数? 合规审计(尤其 AML 场景)要的是「可重建的责任链」:监管或事后复盘时,要能从一次 sar.draft 调用反查到「2026-xx-xx,主体 X 用 jti=Y 的 token(aud 限定本 RS、含 sar:write scope)发起」。业务参数(金额、账户)只回答「做了什么」,sub/aud/jti 才回答「谁有权这样做、凭据是否合法」——后者才是合规要追的那条线。
与相邻概念的边界。 jti 不是 session id:本仓 MCP server 是 stateless(Day 64/B6,每请求新建 server+transport),没有协议级 session;jti 是「token 维度」的唯一标识,不是「连接维度」的。审计行也不等于可观测的 trace span(那是 OTel/Langfuse 的事,本仓标为待接线)——审计行是合规证据(要持久、可不可篡改是重点),trace 是运维观测(可采样、可丢)。两者目标不同。
成功路径 vs 拒绝路径的对称性。 Day 65 处理「该拒就拒」:无 token→401、错 aud→reject、过期→reject、scope 不足→403。今天 Day 66 处理「该放就放」,但它不是 Day 65 的简单取反——成功路径多了一层「放行的同时留痕」的要求。一个只会返回 200、却没留下 sub/aud/jti 的 server,在合规视角下和「裸放行」无异:监管问「这次 sar.draft 是谁授权的」时答不上来。所以成功路径的难点不在「让它通过」,而在「通过时把责任链钉死」。
scope 收敛的粒度。 本仓 server 的 gate 当前只校验一个粗粒度 scope mcp:call(server.ts:48)——「能不能调 MCP」。而 Day 61 草拟的 5×scope 矩阵是更细的目标形态:每个受保护 AML 工具配独立 scope,使「能调 evidence.fetch」不等于「能调 sar.draft」。下面是该矩阵的形态(教学目标,非当前 server 已实现的细粒度):
| 工具 | 所需最小 scope | 成功路径是否注入 |
|---|---|---|
evidence.fetch | aml:evidence:read | 是(调查需读证据) |
typology.match | aml:typology:read | 是(比对类型学) |
sar.draft | aml:sar:write | 否(本次任务不写 SAR) |
audit.write | aml:audit:write | 是(系统侧自动写审计) |
case.read | aml:case:read | 视任务而定 |
成功路径的 scope 收敛即「只注入本次任务真正用到的那几行」——上表中若本次只做证据汇集+类型学比对,就只给 aml:evidence:read+aml:typology:read,不给 aml:sar:write。粗粒度 mcp:call → 细粒度 per-tool scope 是从 demo 走向生产的演进方向(待建)。
来源:OAuth 2.1(draft,2026 现行收敛中)、RFC 8707 Resource Indicators、MCP Security Best Practices(2026-01)。
2. 代码走读(src/agent/mcp/auth.ts + server.ts)
成功路径在本仓由 mintToken → verifyAccessToken → requireScope 三原语 + server 的 200 分支构成。真实符号走读:
mintToken(secret, { sub, scopes, ttlSec?, issuer?, audience? })(auth.ts:17)——用 jose 的SignJWT造一枚 HS256 token:.setSubject(opts.sub)写入sub,scope取opts.scopes.join(' ')(空格分隔,OAuth 约定),.setExpirationTime(nowSec + (ttlSec ?? 3600))。成功路径的 scope 收敛就发生在调用方传入的scopes数组——只放该任务需要的几个。注意:当前mintToken写了sub,但jti未由 mintToken 自动注入(jose 需.setJti()显式设置),所以「审计行写 jti」这一步是消费侧/审计侧的职责,本仓 auth.ts 尚未内建 jti 生成——这点要诚实标注,不能假装 token 自带 jti。verifyAccessToken(token, { secret, issuer?, audience? })(auth.ts:33)——jwtVerify一次性校验签名 +iss/aud/exp/nbf,algorithms: ['HS256']钉死算法(防 alg confusion)。成功时返回payload as TokenClaims。tokenScopes(claims)(auth.ts:45)——把claims.scope按\s+切成数组;requireScope(claims, scope)(auth.ts:50)在缺 scope 时抛insufficient_scope: requires "..."。成功路径下这两个不抛错即放行。- server.ts 的 200 分支(server.ts:40-54)——
startHttpServer在process.env.MCP_AUTH_SECRET存在时启用 gate:bearerFromHeader取 token →verifyAccessToken→requireScope(claims, 'mcp:call')。两步都通过才落到下面createMcpServer()+transport.handleRequest,真正派发tools/call返回 200。成功路径就是「带正确 scope token → 跳过 401 分支 → tools/call 执行」。 - 2 次工具调用挂哪里。 server 注册的工具来自
MCP_TOOLS(src/agent/mcp/tools.ts,含bpe_report/paired_bootstrap/assess_structuring等);成功 transcript 里「2 个工具」即 agent 连续发两条tools/call,各得一条 200。 - 审计行风格参照
contextAudit.ts。 seed 说「复用 contextAudit.ts 风格」——注意 contextAudit.ts 实际做的是上下文 token 预算(auditToolContext/contextBudget,用从零 BPE 估 relative token 成本),不是 OAuth 审计日志。所以「复用其风格」指的是「逐行结构化、纯函数、确定性」这种写法风格,而非直接调用它的函数。审计行字段(sub/aud/jti + tool + ts)需新写,不能误称 contextAudit 已产出 OAuth 审计行。
成功路径报文形态(手算走一遍)。 把一次成功调用拆成可验证的报文序列,便于判定 transcript 是否真成立:
- 签发:
mintToken(secret, { sub:'agent-aml', scopes:['mcp:call'], audience:'momoweb3-aml-mcp', ttlSec:600 })→ 一枚 HS256 JWT,payload 含{ sub:'agent-aml', scope:'mcp:call', aud:'momoweb3-aml-mcp', iat, exp:iat+600 }。 - 请求 1:
POST /mcp带Authorization: Bearer <jwt>,body 为{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"assess_structuring","arguments":{...}}}。 - server:
bearerFromHeader取出 jwt →verifyAccessToken通过(签名+aud+exp 都对)→requireScope(claims,'mcp:call')通过 →tools/call执行 → 返回 200,结果在result.content。 - 审计行 1:
{ ts, sub:'agent-aml', aud:'momoweb3-aml-mcp', jti:'<uuid>', tool:'assess_structuring', ok:true }。 - 请求 2 + 审计行 2:同上换
bpe_report(或另一工具)→ 第二条 200 + 第二条审计行。
判定标准:transcript 里要能看到两条 200 + 两条 sub/aud/jti 齐全的审计行——少任一项都不算成功路径达成。
3. 今日实战
- 用
mintToken(secret, { sub: 'agent-aml-investigator', scopes: ['mcp:call'], audience: 'momoweb3-aml-mcp' })造一枚只含最小 scope 的 token(成功路径的核心动作:不给多余 scope)。 MCP_AUTH_SECRET=<secret> pnpm mcp:serve起 server(server.ts 的 gate 据此门控)。- 让 DeepSeek-V4(provider-agnostic runner 默认 deepseek)带该 token,经 server 连发 2 条
tools/call,各取一条 200。 - 每次调用写一条审计行
{ ts, sub, aud, jti, tool, args_digest }(结构化、确定性,体例对齐 contextAudit.ts 的纯函数行式输出)。 - 把成功 transcript(含 200 + 2 工具 + 审计行)落
docs/aipa/day66-success-path.md,归档进作品集索引。 - 与 Day 65「被拒(403)」transcript 配对,形成「同一 server、一拒一放」的对照证据(Day 70 收口归档)。
4. 今日实测 / 产出
auth.ts的mintToken/verifyAccessToken+ server 200 路径:已 built+tested(无 token→401 已实测,mint 合法 token→200 已实测,整体 423 tests green)。- 「成功」transcript 文件(200 + 2 工具调用 + 审计行)committed —— 端到端 agent 实跑标注为 待跑(需 key 跑),将产出含 2 次 200 工具调用 + 审计行的真实记录。
- 诚实补充:
jti自动注入在 auth.ts 尚未内建(mintToken 仅设sub/scope/exp),审计侧 jti 生成为 待建;不得宣称 token 已自带 jti。
5. 常见误区 / 陷阱
- 「成功就该给宽 scope」:把一枚
*/宽 scope token 复用于所有工具直接违反最小权限——成功路径的难点恰恰是 scope 收敛到最小。 - 把
jti当 session id:stateless server 没有协议级 session,jti是 token 维度唯一 id,别混为会话标识。 - 审计行只记业务参数不记
sub/aud/jti:那样只能回答「做了什么」,回答不了「谁有权这样做」,合规审计链断裂。 - 误以为 contextAudit.ts 已产出 OAuth 审计行:它做的是 token 预算估算,与 OAuth 审计是两回事,只借「纯函数行式」写法风格。
6. 学习资源(每条带 YYYY-MM)
- OAuth 2.1 Authorization Framework(IETF draft,2026 现行收敛中)— scope / token 模型。
- RFC 8707 Resource Indicators for OAuth 2.0(2020-02,现行)—
aud受众限定,防 confused deputy。 - RFC 9728 Protected Resource Metadata(2025-04)— RS 元数据发现。
- RFC 7519 JSON Web Token(2015-05,经典)—
sub/aud/jti/exp标准 claim 定义。 - MCP Security Best Practices(2026-01)— 最小权限 + 审计字段建议。
- jose(npm,维护活跃,2026 执行当周核版本)—
SignJWT.setSubject/setJti、jwtVerify。 - 本仓
src/agent/mcp/auth.ts+server.ts(2026-06)— 成功路径 200 分支、mint/verify/requireScope 三原语。
附:概念辨析(易混淆)
subvsaud:sub是「谁在调」(主体),aud是「token 签给哪个 RS 用」(受众)。校验aud命中自己才放行,是防 confused deputy 的关键。jtivs session id:jti是 token 维度唯一 id(每枚 token 一个);session id 是连接维度(stateless server 没有)。两者不可混。- scope 收敛 vs scope 校验:收敛在「签发面」(只给该给的,Day 66);校验在「执行面」(每次调用 per-call 比对,Day 64)。同一最小权限原则的两端。
- 审计行 vs trace span:审计行是合规证据(持久、append-only、防篡改);trace span 是运维观测(可采样、可丢)。目标不同,不可互替。
- 401 vs 403:401 未认证/token 无效(Day 65);403 已认证但 scope 不足。成功路径是「既过认证又过 scope」。
附:production gap 清单(诚实)
为避免把 demo 误当生产,明确本日成功路径的已建/待建边界:
- 已建:HS256 自签 token 的 mint→verify→requireScope 闭环;server 的 401/200 分支(已实测)。
- 待建:①
jti自动注入与重放检测(auth.ts 未内建.setJti());② per-tool 细粒度 scope(当前只校mcp:call);③ RS256+JWKS(生产正解,当前对称密钥仅供本地/测试);④ 审计行的持久化与防篡改存储(合规要求 append-only)。这些是从「教学装置」走向「可上线 RS」的剩余工作量。
SOTA检查 (2026-06 更新)
- 当前主流:最小权限 token +
jti审计 = MCP Security Best Practices(2026-01)共识做法,仍 SOTA。 - 待复查:MCP 2026-07-28 最终规范定稿后,须复验「审计字段是否被标准化」(若标准化,本仓审计行字段名需对齐规范)。
- 过时黑名单:① 「同一宽 scope token 复用于所有工具」违反最小权限;② 「MCP 无授权层」旧叙事(2025 早期 spec)已过时;③ HS256 当生产方案(应迁 RS256+JWKS)。
- 审计字段是否仍 SOTA:
sub/aud/jti三元组是 RFC 7519 标准 claim + 2026-01 最佳实践的交集,仍 SOTA;待 07-28 spec 看是否补充 MCP 专属审计字段(如 tool 调用链 id)。 - 下次复查点:2026-07-28(MCP spec 定稿,复验审计字段标准化与 RS 元数据端点要求);jose 版本执行当周核对。
自测题(讲得出才算掌握)
- 成功路径为什么不只是「返回 200」?少了什么就不算达成?(答:少 sub/aud/jti 审计行,合规视角等于裸放行。)
aud和sub各回答什么问题?aud校验防的是哪类攻击?(答:sub=谁调、aud=签给哪个 RS;aud 校验防 confused deputy。)- 本仓 server 当前校验的是哪个 scope?细粒度 per-tool scope 为何更安全?(答:
mcp:call;细粒度使「能调 A」不等于「能调 B」。) - 为什么
jti不能当 session id?(答:stateless server 无协议级 session,jti 是 token 维度唯一 id。)
衔接
- 昨天:Day 65 — 拒绝路径与错误语义(WWW-Authenticate):401/403 分清、
error="insufficient_scope"。 - 今天:成功路径 = scope 收敛 +
sub/aud/jti审计行,让每次 200 工具调用可追溯。 - 明天:Day 67 — eval CI gate 设计(M2):从「安全支柱」转入「评测支柱」,把可信基线冻成快照、fail-closed 阻断回归。