Repository navigation
MCP OAuth cannot complete on 17.3.0: plugin-auth passes validAudiences, which @better-auth/oauth-provider 1.7.2 no longer reads — every resource= request fails with invalid_target … is not configured #16530
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Sep 8, 2026 分诊:
domain:services/bug/p1/pm:queue/ typeBug车道:修复面是
packages/plugins/plugin-auth/src/auth-manager.ts:3411的oauthProvider({...})调用 —— 车道表domain:services行(plugin-auth)。⛔ 不是domain:cli:卡面顺带提的"CLI 启动横幅印 http" 是另一个面(见下"拆出")。准入 (a) 可复现缺陷,卡面把三段证据都钉住了:discovery 文档正确 → DCR 成功(
sys_oauth_application6 行)→ 授权端拒绝,且sys_oauth_resource/sys_oauth_client_resource0 行、sys_oauth_access_token0 行。有"该有行而没有行"的正读数,不是靠"没看见 token"推断。我在 origin/main(
5e53d73d)上复核到的,以及卡面没写的一条git grep -n validAudiences origin/main -- packages/plugins/plugin-auth/src/ auth-manager.ts:3445 validAudiences: [this.getAuthIssuer(), this.getMcpResourceUrl()], auth-manager.mcp-oauth.test.ts:301 expect(opts.validAudiences).toContain('…/api/v1/mcp'); auth-manager.mcp-oauth.test.ts:302 expect(opts.validAudiences).toContain('…/api/v1/auth'); git grep -n 'enforcePerClientResources|clientRegistrationDefaultResources|resourcePrivileges' \ origin/main -- packages/plugins/plugin-auth/src/ -> 0(一个都没传) package.json: "@better-auth/oauth-provider": "1.7.2"(钉死版本,不是浮动范围)卡面没写、但解释了"为什么这个洞能活下来"的是那两行测试。
auth-manager.mcp-oauth.test.ts把oauthProvidermock 掉,取mock.calls.at(-1)[0],然后断言我们传进去的那个 options 对象里有validAudiences。它从头到尾没有让 provider 消费过这个选项 —— 所以无论 1.7.2 读不读它,这条断言都绿。一条不可能失败的断言,和一条通过了的断言,是分不出来的;这里正是那种情况。更糟的是它上面那行注释写着「the MCP resource must be a valid audience or token minting fails」,把 1.7.2 上已经不成立的机制当作理由固化了下来。⚠️ 我没能核的一轴:node_modules/@better-auth/oauth-provider/dist/在本环境未安装,所以「1.7.2 的 dist 里validAudiences出现 0 次」这条我没有独立验证,它仍只有卡面作者的一次读数。承接的人请把它当作待复现的第一步,⛔ 不要因为我复核过 in-repo 的部分就顺带认为 vendor 那轴也核过了。p1 的理由
MCP OAuth 在 17.3.0 上完全无法完成:三天六次尝试,一个 token 都没有铸出。且没有应用侧规避 —— 卡面自己指出关键点:匿名 DCR 注册的客户端在注册与登录之间只隔一秒,管理员无法在这中间去
oauthClientResource里补一行链接。不是"慢"或"难用",是这条路不通,而且不通的是对外宣传的 discovery 契约(RFC 9728 → 8414 → DCR → PKCE)所承诺的终点。验收口径(承接 PR 请照抄进
## 验收备注)- 先修测试的可失败性,再修代码。
auth-manager.mcp-oauth.test.ts里那条断言必须改成能被"provider 不消费该选项"这一事实推翻的形状 —— 例如断言真实 provider 在resource=<mcp url>的授权请求下不再返回invalid_target,而不是断言我们传了什么。⛔ 只要断言的对象还是「我们传进去的 options」,这次修完下次换版本还会静默复发。 - 端到端一次:discovery → DCR →
authorize?resource=<mcp url>→ 同意页 → token,且铸出的 token 的 audience 指向 MCP resource。贴出sys_oauth_resource/sys_oauth_client_resource/sys_oauth_access_token三张表修复前后的行数。 - 两条候选路线(注册
oauthResource行 +clientRegistrationDefaultResources,或对 MCP resource 关掉enforcePerClientResources)任选其一即可,但 PR 必须写明为什么选它 —— 后者放宽的是"哪个客户端能拿这个 audience",是安全面,需要一句明确的取舍陈述。 - 顺带清掉
validAudiences这个死选项,⛔ 不要留着"传了但没人读"的字段:它是这次误判的来源。
拆出(不在本卡)
卡面「Side observations」第一条 —— CLI 启动横幅从监听端口而非 canonical origin 拼
http://localhost:4001/api/v1/mcp,在OS_AUTH_URL=https://…下印错 —— 是packages/cli的独立面,与本卡的 provider 选项无关,也不该让这张 p1 去承接一次 CLI 包的验证。已单独归档,⛔ 不要顺手改进本 PR。本席权限声明:分诊席只分类/定级/定车道。⛔ 不认领、⛔ 不派发、⛔ 不写代码、⛔ 不合并、⛔ 不裁决决策箱卡。此卡不入决策箱:方向由 1.7.2 的 API 决定,不迁移存量数据形状,也不删除已发布能力;第 3 条的两条路线是承接者带取舍陈述的工程选择,不是需要维护者裁的岔路。
Generated by Claude Code
- 先修测试的可失败性,再修代码。
- addedpriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 8, 2026 更正:优先级标签打错了词汇表(
p1→priority:p1),已修上一条分诊我给这张卡打的是裸标签
p1。仓库里确实存在这个标签,但取卡序读的不是它:SKILL.md:461的全序是priority:p0>pm:blocking>target:板上项 > p1 > p2 > p3 > 无级,同级先Bug再卡龄,而级别来自priority:p*一族(SKILL.md:117即以priority:p0立表)。所以打成裸
p1的后果不是"标签不好看",是这张卡在队列里排在"无级"档,也就是最后。 一张"MCP OAuth 在 17.3.0 上完全无法完成、三天没铸出过一个 token"的卡,被我标成了取卡序里的最低档。定级是对的,承载定级的字段是错的 —— 而错误的方向恰好是让它看起来已经定过级,所以不会有人来问。已改为
priority:p1,其余标签不变。波及面我量了,不是估的:按裸标签
p1/p2/p3分别列开放卡,总数 1 / 2 / 2,全部是我这一批刚写的五张(#16530、#16535、#16534、#16533、#16734),五张已全部改正。本次会话更早的输出用的都是priority:p*,没有被波及 —— 这个错是这一批里新起的,不是一直在漏。给后来者的一句:仓库里同时存在两套优先级标签,
p2和priority:p2都能打上去、都不会报错。⛔ 不要凭"标上去了"判断标对了 —— 打完之后按priority:p*回查一次,查不到就是没进队列。
Generated by Claude Code
Environment note, for whoever picks this up. Everything in this card was observed through Claude Code inside the Claude desktop app (Claude for Mac 1.46388.x, bundled Claude Code 2.1.260) against a local hotcrm dev server. Because the desktop client refuses to run OAuth against a plain-
http://server, the local setup used a self-signed CA + a small reverse proxy onhttps://localhost:4443withOS_AUTH_URL=https://localhost:4443so the discovery documents advertise https.That client and that proxy are only how the path was reached. The defect itself is server-side and client-independent: any OAuth-capable MCP client (Claude Code CLI, Cursor, VS Code, a claude.ai custom connector) hits the same authorization-server / security-layer behaviour once it gets past TLS. Nothing here depends on the desktop app.
Contract-review handoff — PR #16780 @
5cd3b999a: CHANGES REQUIRED → patch round (director seat, 2026-09-08)Review: #16780 (comment) (isolated
CONTRACT_REVIEW_TIERseat).Reviewed-by:director seatsession_01TezFG8ZMrNH6n5VTNpPpdH(isolated fable subagent).Implemented-by:branchclaude/issue-16530-mcp-oauth-resource-registration.Owed before re-review: F1 — the PR declares
Clause-②: yes(accept set of/oauth2/authorizegrows) while grading@objectstack/plugin-authpatch; per the batch #35 WHICH LEVEL ruling a new accepted value takes at leastminor— change the level (the LEVEL-axis gate could not seepackages/plugins/*, #16713). F2 — add the missing negative control: an unregisteredresource=still answersinvalid_targetagainst the real provider. F3 is a body-wording note, no code.State: carrier removed from card and PR (review concluded); re-hang both with the patched head. Card stays
pm:dispatched, assignee unchanged.
Generated by Claude Code
Re-review PASS → landed (director seat, 2026-09-08 14:1xZ)
- PR: fix(plugin-auth): register the MCP resource so RFC 8707 authorize can succeed #16780 @
28c7da91e— re-review fix(plugin-auth): register the MCP resource so RFC 8707 authorize can succeed #16780 (comment) (claude-fable-5-1, isolated seat). F1–F4 of the first review all discharged by measurement; route (a) as dispatched:enforcePerClientResourcesstays at itstruedefault, the one MCP resource is seeded and linked at registration,allowUnauthenticatedClientRegistrationunchanged. CI 37 success / 0 failure; non-governed. - Handoff by label:
needs:contract-reviewdropped on the PR and on this card; PR undrafted; auto-merge (squash) armed. Card stayspm:dispatcheduntil the merge closes it viaFixes #16530. - For the maintainer, record only (reviewer F3 / N1): after this lands every client created through the provider's registration path holds the MCP audience; the per-client check goes on discriminating only for non-DCR clients. That is the designed outcome for public MCP clients and the route the dispatch ordered; route (b) — switching the check off — stays the maintainer floor and was not taken.
Reviewed-by: director seat (contract review tier
claude-fable-5-1)
Implemented-by:domain:servicesseat (os-trump, sessionsession_012zTkyNHJ7TkuN2oXtP5x37)
Generated by Claude Code
- PR: fix(plugin-auth): register the MCP resource so RFC 8707 authorize can succeed #16780 @
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: fix(plugin-auth): register the MCP resource so RFC 8707 authorize can succeed #16780, merged.
- Closing commit
142c01c88e, merged intomain. - Left untouched:
bug,priority:p1,domain:services,auth,mcp— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34270387866 · trigger
scheduleGenerated by Claude Code
Summary
MCP OAuth cannot complete on 17.3.0: the authorization server rejects every MCP client with
invalid_target: requested resource <mcp url> is not configured.@objectstack/plugin-authconfigures@better-auth/oauth-providerwithvalidAudiences: [authIssuer, mcpResourceUrl], but oauth-provider 1.7.2 (the version plugin-auth declares via^1.7.2) no longer has that option — the stringvalidAudiencesdoes not occur anywhere in its dist. In 1.7.2 an RFC 8707resourceis resolved from theoauthResourcetable (sys_oauth_resource), andenforcePerClientResourcesdefaults totrue, so each OAuth client must also be linked inoauthClientResource. Neither row is ever written, so any client that sendsresource=(Claude Code does) is refused at/oauth2/authorize.This is independent of http/https and of the desktop-app
must be httpsrefusal — it is the next wall behind it.Environment
789a7324(main),@objectstack/*17.3.0,better-auth1.7.2,@better-auth/oauth-provider1.7.2 (hoisted; plugin-auth declares^1.7.2for both)objectstack dev -p 4001withOS_AUTH_URL=https://localhost:4443, behind a local TLS reverse proxy (self-signed CA trusted in the system keychain){type: http, url: https://localhost:4443/api/v1/mcp}, no headersReproduction
Discovery is correct after
OS_AUTH_URL:/.well-known/oauth-protected-resource→{"resource":"https://localhost:4443/api/v1/mcp","authorization_servers":["https://localhost:4443/api/v1/auth"], …}/.well-known/oauth-authorization-server→ issuer / authorization / token / registration endpoints allhttps://localhost:4443/api/v1/auth/…GET /api/v1/mcp→401,www-authenticate: Bearer realm="ObjectStack MCP", resource_metadata="https://localhost:4443/.well-known/oauth-protected-resource"In Claude Code:
/mcp→ Connect. Dynamic client registration succeeds (a newsys_oauth_applicationrowClaude Code (hotcrm)with scopes["openid","profile","email","offline_access","data:read","data:write","actions:execute"]), the system browser opens, and the flow ends on the error page:Database after the attempt:
sys_oauth_resource0 rows,sys_oauth_client_resource0 rows,sys_oauth_application6 rows (one per Connect attempt over three days — a fresh client each time),sys_oauth_consent/sys_oauth_access_token/sys_oauth_refresh_tokenall 0. No token has ever been minted.Where it happens
@objectstack/plugin-auth(dist,oauthProvider({...})options):validAudiences: [this.getAuthIssuer(), this.getMcpResourceUrl()]with the comment "the AS only mints audiences it knows". Noresources,clientRegistrationDefaultResources,enforcePerClientResourcesorresourcePrivilegesare passed.@better-auth/oauth-provider1.7.2, token/authorize path: for every requested resourcegetResource(ctx, opts, identifier)reads theoauthResourcemodel; a miss throwsinvalid_target … is not configured. ThenresolveEnforcePerClientResources(opts)→{ value: true, source: "default" }andassertClientLinkedToResourcesrequires anoauthClientResourcerow per client.grep -c validAudiences node_modules/@better-auth/oauth-provider/dist/*.mjs→ 0 in every file.Expected
An MCP client following the advertised discovery documents (RFC 9728 → RFC 8414 → DCR → PKCE authorize with
resource=<mcp url>) should reach the consent screen and receive a token audienced to the MCP resource. Concretely, plugin-auth should:getMcpResourceUrl()) as anoauthResourcerow at boot (or via the plugin'sresourcesoption, whichever 1.7.2 honours), andclientRegistrationDefaultResources: [mcpResourceUrl]so DCR-registered clients are linked automatically, or setenforcePerClientResources: falsefor the MCP resource — a client that registers anonymously one second before the login cannot be linked by an admin in between.Side observations
http://localhost:4001/api/v1/mcpand aclaude mcp add … http://localhost:4001/…line even whenOS_AUTH_URL=https://localhost:4443is set and the➜ MCP:line above it correctly says https. The hint is built from the listen port, not the canonical origin.AuthManagerstill carries two more independentbasePathnormalisers —getAuthIssuer()andgetMcpResourceUrl()— and one of them builds a malformed URL #16399 (URL normalisation ingetAuthIssuer/getMcpResourceUrl). The values here were well-formed; the problem is that they are handed to an option the provider no longer reads.run_actionon a screen flow dead-ends: the screen pauses even when every input is bound, andlist_actionsnever surfaces flow input variables #15705 / MCPrun_actionon a flow action answersok: trueand starts the run on a row the caller cannot read (or that does not exist) — the flow door still turns a denied load into an implicit grant #16370 (MCPrun_actionon screen flows).