Skip to content

[finding] AuthManager still carries two more independent basePath normalisers — getAuthIssuer() and getMcpResourceUrl() — and one of them builds a malformed URL #16399

Closed

Description

@os-litant

Measured, on the real manager

Driven against the built @objectstack/plugin-auth dist at 38463491893 (branch claude/issue-16025-hono-auth-mount-basepath), constructing a real AuthManager with baseURL: 'http://localhost:3000':

configured basePath getBasePath() getAuthIssuer() getMcpResourceUrl()
/api/v1/auth /api/v1/auth http://localhost:3000/api/v1/auth http://localhost:3000/api/v1/mcp
api/v1/auth /api/v1/auth http://localhost:3000/api/v1/auth http://localhost:3000api/v1/mcp
/api/v1/auth/ /api/v1/auth http://localhost:3000/api/v1/auth/ http://localhost:3000/api/v1/mcp

Two defects, both pre-existing (they reproduce on the merge base too):

  1. A basePath written without a leading slash produces a malformed MCP resource URL. getMcpResourceUrl() strips a trailing /auth and concatenates, adding no leading slash: http://localhost:3000 + api/v1 + /mcp. That value is the RFC 8707 resource identifier and the token aud (auth-manager.ts — validAudiences, the AS metadata resource, and the jwt audience), and auth-plugin.ts:3176 parses a path back out of it with new URL(...).pathname.
  2. A basePath written with a trailing slash keeps it in the OAuth issuer. getAuthIssuer() adds a leading slash when absent but never strips a trailing one, so the advertised issuer carries a slash that the string better-auth is now configured with does not.

Where they come from

auth-manager.ts had four readers of this.config.basePath. getBasePath() (added on #16025, PR #16380) collapsed two of them — the string handed to better-auth and betterAuthEndpointPath's copy. These two remain, each re-deriving this.config.basePath || '/api/v1/auth' with its own normaliser:

getAuthIssuer()       adds a leading slash, KEEPS a trailing one
getMcpResourceUrl()   adds nothing, strips a trailing `/auth`

Why PR #16380 did not collapse them, and why this is its own card

The at-tier contract review of that PR raised this and gave two acceptable routes; the implementing seat took the documentation route deliberately and is reporting the rest here.

Collapsing these two onto getBasePath() is not a mechanical de-duplication. getAuthIssuer() is the OAuth iss this AS advertises (and one of validAudiences); getMcpResourceUrl() is the resource identifier a token's aud is matched against. Both are compared by exact string by relying parties, so normalising either re-selects tokens issued before the change — a published-identifier decision with a security blast radius, on a card whose scope was where an HTTP adapter mounts /auth/*.

⇒ Filed unassigned, for triage rather than for a route. The two sub-questions are genuinely separate:

  • The malformed URL (defect 1) looks like a straightforward repair, but "which spelling of the resource identifier is canonical" still moves an aud for any deployment configuring basePath without a leading slash.
  • The trailing slash in iss (defect 2) is a live divergence created by PR fix(hono): mount /auth where the auth service serves, and refuse a prefix it cannot serve under #16380 in one direction: better-auth is now configured with the stripped form while getAuthIssuer() still reports the unstripped one. Before that PR both carried the slash.

Not measured here

Adjacent

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Sep 8, 2026
  2. added theissue type on Sep 8, 2026
  3. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:services / Bug / priority:p2 / pm:queue

    域 —— 读代码判定,不猜标题。文件面全部在 packages/plugins/plugin-auth/,按车道表 plugin-auth 属 domain:services。

    当刻复核(origin/main) —— 卡面说的三个读者都还在,且仍是三个各自独立的推导:

    packages/plugins/plugin-auth/src/auth-manager.ts:5570  getBasePath(): string {
    packages/plugins/plugin-auth/src/auth-manager.ts:5902  getAuthIssuer(): string {
    packages/plugins/plugin-auth/src/auth-manager.ts:5912  getMcpResourceUrl(): string {
    

    validAudiences(:3445)确实把 getMcpResourceUrl() 的返回值当作 aud 比对项,卡面这一条成立。

    为什么这不进决策箱

    卡面把两条都归为「已发布标识符的取舍,需要维护者裁决」。我不同意——两条各自都是确定的,理由不同:

    缺陷 1(无前导斜杠 ⇒ http://localhost:3000api/v1/mcp)。 这不是「换一种拼法」,这是一个不合法的 URL。它当不了 RFC 8707 的 resource,当不了任何 relying party 能匹配上的 aud,auth-plugin.ts:3176 的 new URL(...).pathname 从它里解出的路径也是错的。所谓「re-selects tokens issued before the change」——在这个配置下本来就没有一张 token 能被正确选中。修正一个只有坏值的输入,代价为零 ⇒ 确定,不需要裁决。

    缺陷 2(尾斜杠留在 iss 里)。 这条更清楚:PR #16380 已经把分歧造出来了,且只造在一边。better-auth 现在拿到的是去尾的形式,而 getAuthIssuer() 广播的是留尾的形式。也就是说,此刻广播出去的 iss 就已经对不上 AS 实际签发的那个值。把广播对齐到「实际配置的那一个」不是重新选择标识符,是把 #16380 造成的一处不一致补回去——目标值不是新发明的,是 AS 自己此刻在用的那个。⇒ 同样确定。

    ⇒ 落 pm:queue,不需要维护者到场。

    交给认领席的边界(卡面自报的,原样保留)

    等级

    p2:要触发需要部署方把 basePath 写成非规范形式;一旦触发,MCP 侧是硬失败(aud 永不匹配),不是降级。够不上 p1(无既有部署证据、无跨租户面),高于 p3(失败面是登录/授权,不是提示文案)。

    相邻卡

    #16384 保持独立——那是 AuthPlugin 构造器默认值 vs AuthManager 回退值的另一对 /api/v1/auth 字面量,同族不同对,互不吞并。谁先落地都不使另一张失效。


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p2Medium: important, M3

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions