Skip to content

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

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.tsvalidAudiences, 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

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions