Repository navigation
fix(plugin-auth): register an MCP client that omits application_type with a loopback or custom-scheme redirect as native - #25156
Conversation
…with a loopback or custom-scheme redirect as native Pass clientRegistrationDefaultApplicationType: 'infer' to @better-auth/oauth-provider. A dynamic registration with no application_type is classified native when any redirect URI is a non-http(s) scheme or http on exactly localhost, 127.0.0.1 or [::1], and every redirect is then held to the native rule; otherwise it stays web. The new test boots the real provider from AuthManager's options and pins the measured boundary, including the confidential cell and the PKCE requirement at authorize. Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e742193a349a4afd99a2fee5dc77b9668c8c2746 && git checkout e742193a349a4afd99a2fee5dc77b9668c8c2746
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 23540582f4f7a783ee11337bbda12a8352a70f91 6d4fd2a5a2606a58a6a2f77b1a05e0953a17ebeb && git checkout -B drift-repro 23540582f4f7a783ee11337bbda12a8352a70f91 && git merge --no-ff 6d4fd2a5a2606a58a6a2f77b1a05e0953a17ebeb
node scripts/docs-audit/affected-docs.mjs --json 23540582f4f7a783ee11337bbda12a8352a70f91 |
席位核对
Landing: ordinary queue landing. The seat sets ready and auto-merge through the relay in this act. After the merge, #25118 closes by Changeset sentences checked against the diff:
All hold. Generated by Claude Code |
Fixes #25118
Clause-②: no (a runtime security-boundary change the maintainer ruled; no published type, export or metadata key moves)
Metadata-protocol: none
What changes
plugin-auth now passes
clientRegistrationDefaultApplicationType: 'infer'to@better-auth/oauth-provider1.7.12. A comment names this card, ruling6329352251(letter A1) and the measured boundary. The whole production change is that one option line in theoauthProvider({ ... })options ofpackages/plugins/plugin-auth/src/auth-manager.ts. There is no hook on/oauth2/registerand no second classification rule (ruling A1, items 1 and 4).application_typewas filled asweb. The web redirect rule refuses a loopbackhttpor custom-scheme redirect, so every local MCP client (Claude Code, Cursor, VS Code, Codex CLI) was refused at registration withinvalid_redirect_uri.nativewhen any redirect URI uses a non-http(s) scheme orhttpon exactlylocalhost,127.0.0.1or[::1], and it then holds each redirect to the native rule. Every other body staysweb. Anapplication_typethe client sends is never overridden.registerEndpointin the installed dist). The Setup form's managedcreate-clientdoor (/sys-oauth-application/register) is not affected. The option does nothing while DCR is off./oauth2/register. The global before-hook has no branch for that path, andauth-plugin.tsmounts no raw route on it.The measured boundary, pinned on the real provider
The new file
packages/plugins/plugin-auth/src/auth-manager.mcp-oauth-dcr-infer.test.tsboots the real better-auth 1.7.12 and@better-auth/oauth-provider1.7.12 handler from the exact optionsAuthManagerproduces (MCP on, so DCR on), then drivesPOST /oauth2/registerandGET /oauth2/authorize.Every body omits
application_type, as the clients send it. The first cell sends the card's own reproduction body and asserts that it has noapplication_typekey. The existingauth-manager.mcp-oauth-resource.test.tsregisters withapplication_type: nativespelled out, which is why it stayed green through this defect.application_typeunless stated)http://localhostredirectnative, no secrethttp://127.0.0.1redirectnativecursor://) redirectnativehttps://evil.example/cbaloneweb(unchanged)http://evil.example/cbinvalid_redirect_uri, no client storedhttp://localhost.evil.example/cbinvalid_redirect_uri, no client storedapplication_type: webinvalid_redirect_uri(a sent type is never overridden)client_secret_basic), loopbacknative, issued aclient_secretclient_secret_basic), loopbacknative, issued aclient_secretclient_secret_basic), custom schemenative, issued aclient_secrethttps://evil.example/cbin one bodynative(any qualifying redirect classifies the body)OS_OIDC_DCR_ENABLED=false), the card's bodyaccess_denied, no client storedtoken_endpoint_auth_method. The same confidential body withapplication_type: nativespelled out is already accepted onmain, so no registrable client shape is added; only the meaning of an omitted field changes (ruling A1, item 3).clientRegistrationRequirePKCEkeeps itstruedefault.error=invalid_request, a description that names PKCE, and no code. The paired request is identical apart from the S256 PKCE parameters (same client, session, redirect, scopes and resource) and reaches the consent page, so the refusal cannot have another cause.auth-manager.mcp-oauth-resource.test.ts) now also covers the new option name.Verification
The package readings below were taken at
5a7e1f02a, on the shared container. The head6d4fd2a5ais that commit merged withorigin/main9cfc57533. The four incoming commits touchcloud-connection,runtime,service-automation,verify, docs and an ADR, and none of those is in plugin-auth's dependency closure. So the package readings stand (the scoped re-check of AGENTS §10), and the gate union was re-run on6d4fd2a5a. The test imports./auth-managerfrom source, so the ablation needed no rebuild to reach the run.pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/auth-manager.mcp-oauth-dcr-infer.test.tsgaveTest Files 1 passed (1),Tests 12 passed (12), lockVERDICT command-exit 0.scripts/ablation-replace.mjsdeleted the option line. Proof that it landed: anchor count 1 then 0, blob9c00cadbthena4a53154. Result:Tests 7 failed | 5 passed (12), lockVERDICT command-exit 1.127.0.0.1, custom scheme, the three confidential cells, the mixed body). Each got400 invalid_redirect_uri: web clients require https redirect URIs on non-loopback hosts, the production symptom.httpsalone staysweb; the two foreignhttphosts and explicitwebstay refused; DCR off is 403).9c00cadb,git diff HEADempty,git status --porcelainempty.pnpm --filter @objectstack/plugin-auth testgaveTest Files 182 passed (182),Tests 3450 passed | 10 skipped (3460), lockVERDICT command-exit 0.pnpm --filter @objectstack/plugin-auth typecheckexited 0. That covers the source, the examples, and the test layer undertsconfig.test.json(debt ledger held, no growth).pnpm --filter @objectstack/plugin-auth buildexited 0. The builtdist/*.d.tshas 0 hits forclientRegistrationDefaultApplicationType, whiledist/index.mjshas 1 (positive control), so no published type moves.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 70 commands at6d4fd2a5a(merge base9cfc57533). All 70 ran and exited 0, with each code captured before any pipe. The--ranreconciliation reads "70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN". The rest of the farm and the type-check lanes run in CI.Acceptance owed after this lands
Ruling
6329352251item 5 (carried from6327761812) requires a browser walk on a deployed environment after the cloud pin moves:claude mcp add --transport http NAME https://ENV-HOST/api/v1/mcp, then/mcp, then Authenticate, completing in the browser and listing the tools. That walk makes the environment's Setup, Integrations, Connect an Agent page true. Not measured here: it needs a deployed environment.Acceptance notes
rawApp.post(... /sys-oauth-application/register ...)inauth-plugin.tsstill says DCR on/oauth2/registeris something "we don't want to enable broadly". DCR has been on by default whenever the MCP server is on (resolveDcrEnabled), so that sentence is stale. It is noted only: it is outside this card's file surface and changes no behaviour.describetitles inauth-manager.mcp-oauth-resource.test.tsstill say "installed 1.7.2" while the installed provider is 1.7.12. This is naming only, noted and not changed.Generated by Claude Code