fix(core): validate oauth expires_in values#5356
Conversation
528d67f to
3457f4e
Compare
|
@qwen-code /triage |
|
Thanks for the PR, @tt-a1i! Template: the PR body uses custom headings ("Summary", "Test plan") instead of the template sections. Missing: "Why it's needed", structured "Reviewer Test Plan" (How to verify / Evidence / Tested on table), "Risk & Scope", and "中文说明". The essentials are there — "Fixes #5355" links the issue, and the test commands are listed — but please align with the template so reviewers get the context they need. Not blocking the review on this, but worth fixing. Direction: clearly aligned. Issue #5355 describes a real correctness bug — Approach: minimal and focused. Extract a Moving on to code review. 🔍 中文说明感谢贡献,@tt-a1i! 模板: PR 使用了自定义标题("Summary"、"Test plan"),与模板要求的章节不一致。缺少:"Why it's needed"、结构化的 "Reviewer Test Plan"(验证步骤 / 证据 / 测试平台表格)、"Risk & Scope" 和 "中文说明"。核心信息都有——"Fixes #5355" 关联了 issue,测试命令也列出来了——但请对齐模板以便审查者获取所需上下文。不阻塞审查,但建议修正。 方向: 明确对齐。Issue #5355 描述了一个真实的正确性 bug—— 方案: 最小化且聚焦。提取 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading the diff): given the bug — Comparison: the PR does exactly this. No correctness bugs, no security issues, no AGENTS.md violations. The error re-throw in the JSON catch block (checking Test ResultsAll 27 tests pass (22 existing + 5 new). Typecheck and lint clean. Real-Scenario TestingN/A for this fix — the 中文说明代码审查独立方案(阅读 diff 之前): 针对 对比: PR 完全符合这个方案。 测试结果27 个测试全部通过(22 个已有 + 5 个新增)。Typecheck 和 lint 均通过。 真实场景测试不适用—— — Qwen Code · qwen3.7-max |
|
This is a clean, well-scoped bug fix. The My independent proposal matched the PR's approach almost exactly, which gives me confidence this is the right solution. The only minor observation is that Template compliance should be tightened up (see Stage 1), but that's cosmetic. The substance is solid. Approving. ✅ 中文说明这是一个干净、范围合理的 bug 修复。 我的独立方案与 PR 方案几乎完全一致,这使我对这是正确的解决方案有信心。唯一的小观察是 模板合规性需要改进(见 Stage 1),但那是表面问题。实质内容扎实。 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM — clean, minimal bug fix with solid test coverage. Template compliance could be tightened (noted in review), but the substance is ready to ship. ✅
✅ Local runtime verification (real tmux, live OAuth token server) — PR #5356Verdict: fixes #5355 as intended, no regressions, type-checks & lints clean — safe to merge. I drove the real What the PR doesAdds Evidence — PR headStatic checks Real runtime — live token endpoint A/B vs
|
✅ Local verification report — PR #5356
|
| Check | Result |
|---|---|
PR oauth suite (oauth-provider.test.ts) |
✅ 27/27 pass (22 pre-existing + 5 new) |
| Genuine-regression — PR's tests on BASE source | ✅ exactly the 5 new tests fail, 22 pre-existing pass → tests truly catch the bugs |
Adversarial parseExpiresIn probe (26 inputs, 11 contract assertions) |
✅ all pass (see below) |
tsc --noEmit core |
✅ 0 errors, identical to base → 0 new type errors |
npm run build core |
✅ exit 0 |
prettier --check + eslint (both files) + git diff --check |
✅ all exit 0 |
Genuine-regression detail (base failure signatures)
× preserve expires_in=0 as an immediate expiry → expected undefined to be 1700000000000
× normalize JSON string expires_in values → expected '3600' to be 3600
× reject malformed JSON expires_in values → promise resolved instead of rejecting
× reject malformed form-urlencoded expires_in values→ promise resolved instead of rejecting
× preserve expires_in=0 when refreshing → expiresAt not set
Adversarial parser sweep (PR vs base)
The new parser handles every edge case correctly, where main's parseInt silently corrupted many:
| input | PR parseExpiresIn |
base parseInt |
|---|---|---|
0 (number) |
0 (immediate expiry ✅) |
dropped (falsy) ❌ |
'3600' |
3600 |
3600 |
' 3600 ', '007' |
3600, 7 |
same |
'3600abc' |
throws ✅ | 3600 (truncated) ❌ |
'3.5' / '0x10' / '1e3' / '1e21' |
throws ✅ | 3 / 0 / 1 / 1 ❌ |
-1, 3.5, MAX_SAFE+1, NaN, Infinity, true, {} |
throws ✅ | various silent values ❌ |
null / undefined |
undefined |
undefined |
Minor note (non-blocking)
An empty expires_in= in a form-urlencoded response now throws (Invalid expires_in value:), whereas main silently treated it as undefined (no expiry). An empty value is non-conformant (RFC 6749 says expires_in is optional → a compliant server omits it entirely, which still yields undefined correctly). So this is a defensible fail-closed tightening, but it does mean a buggy server that emits an explicit empty expires_in= would now have its token rejected rather than treated as non-expiring. Worth a mention; not a blocker.
Reproduce
git worktree add --detach /tmp/wt-after $(gh pr view 5356 --json headRefOid -q .headRefOid)
git worktree add --detach /tmp/wt-before origin/main
for w in wt-after wt-before; do ln -s "$PWD/node_modules" /tmp/$w/node_modules; \
ln -s "$PWD/packages/core/node_modules" /tmp/$w/packages/core/node_modules; done
(cd /tmp/wt-after/packages/core && ../../node_modules/.bin/vitest run --coverage.enabled=false src/mcp/oauth-provider.test.ts) # 27/27
# Catch the bug: copy PR's test onto BASE source -> 5 fail
cp /tmp/wt-after/packages/core/src/mcp/oauth-provider.test.ts /tmp/wt-before/packages/core/src/mcp/oauth-provider.test.ts
(cd /tmp/wt-before/packages/core && ../../node_modules/.bin/vitest run --coverage.enabled=false src/mcp/oauth-provider.test.ts)🇨🇳 中文版(点击展开)
✅ 本地验证报告 — PR #5356 fix(core): validate oauth expires_in values
结论:强烈建议合并。 这是针对一个真实 OAuth bug 的、范围聚焦且正确的修复。新测试确实能抓到这些 bug,新的 parseExpiresIn 解析器在大量边界用例下都稳健,且无回归、无类型/lint/格式问题。有一点小的行为变化供你知悉(非阻塞)。
修了什么
expires_in=0被丢弃:main用 falsy 判断(if (tokenResponse.expires_in)),所以“立即过期”的0不会设置expiresAt→ token 被当作永不过期、永不刷新。PR 改为设置expiresAt = now。parseInt静默截断:main用parseInt(expiresIn, 10),于是'3600abc' → 3600、'3.5' → 3、'0x10' → 0、'1e3' → 1都被静默接受。PR 用严格的/^\d+$/解析并对非法值 抛出Invalid expires_in value。- JSON 值未归一化:JSON 里的
expires_in: '3600'(字符串)在main上仍是字符串;PR 让 JSON 和 form-urlencoded 两条路径都经过同一个parseExpiresIn,最终始终是归一化后的数字(或undefined)。
验证方式
在 tmux 中、两个隔离 worktree 里运行真实 vitest:AFTER = 3457f4e8(PR head),BEFORE = a4797b9a(origin/main),软链接复用仓库 node_modules。Node v22.22.2、vitest 3.2.4。可干净合并;目标文件在 merge-base 与 main 之间一致。
| 检查项 | 结果 |
|---|---|
PR oauth 测试(oauth-provider.test.ts) |
✅ 27/27 通过(22 既有 + 5 新增) |
| 回归测试有效性 —— PR 的测试跑 BASE 源码 | ✅ 恰好这 5 个新测试失败,22 个既有通过 → 测试确实抓住了 bug |
对抗性 parseExpiresIn 探测(26 个输入、11 条契约断言) |
✅ 全通过(见下表) |
tsc --noEmit core |
✅ 0 错误,与 base 一致 → 0 新增类型错误 |
npm run build core |
✅ exit 0 |
prettier --check + eslint(两个文件)+ git diff --check |
✅ 均 exit 0 |
回归细节(base 上的失败特征)
× expires_in=0 立即过期 → expected undefined to be 1700000000000
× 归一化 JSON 字符串 expires_in → expected '3600' to be 3600
× 拒绝非法 JSON expires_in → promise 被 resolve 而非 reject
× 拒绝非法 form-urlencoded expires_in→ promise 被 resolve 而非 reject
× 刷新时保留 expires_in=0 → expiresAt 未设置
解析器对抗性扫描(PR vs base)
新解析器对每个边界用例都处理正确,而 main 的 parseInt 会静默破坏很多:
| 输入 | PR parseExpiresIn |
base parseInt |
|---|---|---|
0(数字) |
0(立即过期 ✅) |
被丢弃(falsy)❌ |
'3600' |
3600 |
3600 |
' 3600 '、'007' |
3600、7 |
同左 |
'3600abc' |
抛错 ✅ | 3600(截断)❌ |
'3.5' / '0x10' / '1e3' / '1e21' |
抛错 ✅ | 3 / 0 / 1 / 1 ❌ |
-1、3.5、MAX_SAFE+1、NaN、Infinity、true、{} |
抛错 ✅ | 各种静默值 ❌ |
null / undefined |
undefined |
undefined |
小提示(非阻塞)
form-urlencoded 响应里空的 expires_in= 现在会抛错(Invalid expires_in value:),而 main 会静默当作 undefined(不过期)。空值本身不符合规范(RFC 6749 中 expires_in 可选 → 合规服务器会直接省略它,这种情况仍正确得到 undefined)。因此这是一个合理的 fail-closed 收紧;但确实意味着:若某个有 bug 的服务器显式发送空的 expires_in=,其 token 现在会被拒绝而不是当作不过期。值得一提,但非阻塞。
Verified locally with real vitest under tmux on isolated worktrees of the PR head and origin/main; bug reality proven by running the PR's tests against base source; the parseExpiresIn parser audited with a 26-input adversarial sweep against the old parseInt behavior.
Fixes #5355
Summary
expires_in=0as an immediate OAuth token expiry instead of droppingexpiresAtexpires_infrom JSON and form-urlencoded token responses through one parserexpires_invalues thatparseIntpreviously truncatedTest plan
npx vitest run src/mcp/oauth-provider.test.tsnpm run typecheck --workspace=packages/corenpm run build --workspace=packages/corenpx prettier --check packages/core/src/mcp/oauth-provider.ts packages/core/src/mcp/oauth-provider.test.tsgit diff --checknpm run lintAI Assistance Disclosure
I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.