You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[finding] AuthManager still carries two more independent basePath normalisers — getAuthIssuer() and getMcpResourceUrl() — and one of them builds a malformed URL #16399
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):
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.
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.
Measured, on the real manager
Driven against the built
@objectstack/plugin-authdist at38463491893(branchclaude/issue-16025-hono-auth-mount-basepath), constructing a realAuthManagerwithbaseURL: 'http://localhost:3000':basePathgetBasePath()getAuthIssuer()getMcpResourceUrl()/api/v1/auth/api/v1/authhttp://localhost:3000/api/v1/authhttp://localhost:3000/api/v1/mcpapi/v1/auth/api/v1/authhttp://localhost:3000/api/v1/authhttp://localhost:3000api/v1/mcp/api/v1/auth//api/v1/authhttp://localhost:3000/api/v1/auth/http://localhost:3000/api/v1/mcpTwo defects, both pre-existing (they reproduce on the merge base too):
basePathwritten without a leading slash produces a malformed MCP resource URL.getMcpResourceUrl()strips a trailing/authand concatenates, adding no leading slash:http://localhost:3000+api/v1+/mcp. That value is the RFC 8707resourceidentifier and the tokenaud(auth-manager.ts—validAudiences, the AS metadataresource, and the jwtaudience), andauth-plugin.ts:3176parses a path back out of it withnew URL(...).pathname.basePathwritten 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 advertisedissuercarries a slash that the string better-auth is now configured with does not.Where they come from
auth-manager.tshad four readers ofthis.config.basePath.getBasePath()(added on #16025, PR #16380) collapsed two of them — the string handed to better-auth andbetterAuthEndpointPath's copy. These two remain, each re-derivingthis.config.basePath || '/api/v1/auth'with its own normaliser: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 OAuthissthis AS advertises (and one ofvalidAudiences);getMcpResourceUrl()is the resource identifier a token'saudis 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:
audfor any deployment configuringbasePathwithout a leading slash.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 whilegetAuthIssuer()still reports the unstripped one. Before that PR both carried the slash.Not measured here
basePathwithout a leading slash or with a trailing one.@objectstack/honohas no in-repo consumer (adapters/hono 的 ${prefix}/auth/* 与 ${prefix}/storage/* 是终结式通配 —— 与 #4088 同一缺陷,只是这个包在仓内没有消费者 #4117), and nothing here can answer for out-of-repo hosts.isschange would break a live relying party. That is the decision, not a fact this card can settle.Adjacent
/api/v1/authis declared as two independent literals — AuthPlugin's constructor default and AuthManager's fallback — and a divergence between them is silent #16384 — the other basePath duplication on this manager (AuthPlugin's constructor default vsAuthManager's fallback, two literals of/api/v1/auth). Different pair, same family; neither subsumes the other.getBasePath()is not the single definition, and point here.