Repository navigation
Commit 6b44efa
fix(plugin-auth): register an MCP client that omits application_type with a loopback or custom-scheme redirect as native (#25156)
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-provider` 1.7.12. A comment names this
card, ruling `6329352251` (letter A1) and the measured boundary. The
whole production change is that one option line in the `oauthProvider({
... })` options of `packages/plugins/plugin-auth/src/auth-manager.ts`.
There is no hook on `/oauth2/register` and no second classification rule
(ruling A1, items 1 and 4).
- **Before:** a dynamic registration that omitted `application_type` was
filled as `web`. The web redirect rule refuses a loopback `http` or
custom-scheme redirect, so every local MCP client (Claude Code, Cursor,
VS Code, Codex CLI) was refused at registration with
`invalid_redirect_uri`.
- **After:** the provider's own classifier treats such a body as
`native` when any redirect URI uses a non-http(s) scheme or `http` on
exactly `localhost`, `127.0.0.1` or `[::1]`, and it then holds each
redirect to the native rule. Every other body stays `web`. An
`application_type` the client sends is never overridden.
- **Scope:** the classifier runs only on the dynamic registration source
(`registerEndpoint` in the installed dist). The Setup form's managed
`create-client` door (`/sys-oauth-application/register`) is not
affected. The option does nothing while DCR is off.
- **No layer in front:** plugin-auth puts nothing in front of
`/oauth2/register`. The global before-hook has no branch for that path,
and `auth-plugin.ts` mounts 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.ts`
boots the real better-auth 1.7.12 and `@better-auth/oauth-provider`
1.7.12 handler from the exact options `AuthManager` produces (MCP on, so
DCR on), then drives `POST /oauth2/register` and `GET
/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 no
`application_type` key. The existing
`auth-manager.mcp-oauth-resource.test.ts` registers with
`application_type: native` spelled out, which is why it stayed green
through this defect.
| Cell (no `application_type` unless stated) | Registration | Authorize
|
|---|---|---|
| The card's body: public client, `http://localhost` redirect | 201
`native`, no secret | refused without PKCE, consent with PKCE |
| Public, `http://127.0.0.1` redirect | 201 `native` | refused without
PKCE, consent with PKCE |
| Public, custom-scheme (`cursor://`) redirect | 201 `native` | refused
without PKCE, consent with PKCE |
| Public, `https://evil.example/cb` alone | 201 `web` (unchanged) |
refused without PKCE, consent with PKCE |
| Public, `http://evil.example/cb` | 400 `invalid_redirect_uri`, no
client stored | n/a |
| Public, `http://localhost.evil.example/cb` | 400
`invalid_redirect_uri`, no client stored | n/a |
| Public, loopback, explicit `application_type: web` | 400
`invalid_redirect_uri` (a sent type is never overridden) | n/a |
| Confidential (`client_secret_basic`), loopback | 201 `native`,
**issued a `client_secret`** | refused without PKCE, consent with PKCE |
| Auth method omitted (filled as `client_secret_basic`), loopback | 201
`native`, **issued a `client_secret`** | refused without PKCE, consent
with PKCE |
| Confidential (`client_secret_basic`), custom scheme | 201 `native`,
**issued a `client_secret`** | refused without PKCE, consent with PKCE |
| Public, loopback plus `https://evil.example/cb` in one body | 201
`native` (any qualifying redirect classifies the body) | refused without
PKCE, consent with PKCE |
| DCR off (`OS_OIDC_DCR_ENABLED=false`), the card's body | 403
`access_denied`, no client stored | n/a |
- **Not "public clients only".** The confidential rows are stated as
they behave. The classifier reads redirect URIs and never
`token_endpoint_auth_method`. The same confidential body with
`application_type: native` spelled out is already accepted on `main`, so
no registrable client shape is added; only the meaning of an omitted
field changes (ruling A1, item 3).
- **PKCE.** PKCE stays required at authorize for every registered
client: always for a public client, and for a confidential DCR client
because `clientRegistrationRequirePKCE` keeps its `true` default.
- **The PKCE check is a differential.** "Refused without PKCE" means a
redirect back to the client's own redirect URI with
`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.
- **Liveness.** The existing test "every option AuthManager passes
occurs in the installed provider"
(`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 head `6d4fd2a5a` is that commit merged with `origin/main`
`9cfc57533`. The four incoming commits touch `cloud-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
on `6d4fd2a5a`. The test imports `./auth-manager` from source, so the
ablation needed no rebuild to reach the run.
- **New file:** `pnpm --filter @objectstack/plugin-auth exec vitest run
--maxWorkers=2 src/auth-manager.mcp-oauth-dcr-infer.test.ts` gave `Test
Files 1 passed (1)`, `Tests 12 passed (12)`, lock `VERDICT command-exit
0`.
- **Ablation (one-time, not committed):** `scripts/ablation-replace.mjs`
deleted the option line. Proof that it landed: anchor count 1 then 0,
blob `9c00cadb` then `a4a53154`. Result: `Tests 7 failed | 5 passed
(12)`, lock `VERDICT command-exit 1`.
- Red, as predicted: all seven native cells (the card's body,
`127.0.0.1`, custom scheme, the three confidential cells, the mixed
body). Each got `400 invalid_redirect_uri: web clients require https
redirect URIs on non-loopback hosts`, the production symptom.
- Green, as predicted: the five cells that do not depend on the option
(`https` alone stays `web`; the two foreign `http` hosts and explicit
`web` stay refused; DCR off is 403).
- Restore proven: blob equals HEAD `9c00cadb`, `git diff HEAD` empty,
`git status --porcelain` empty.
- **Package suite:** `pnpm --filter @objectstack/plugin-auth test` gave
`Test Files 182 passed (182)`, `Tests 3450 passed | 10 skipped (3460)`,
lock `VERDICT command-exit 0`.
- **Typecheck:** `pnpm --filter @objectstack/plugin-auth typecheck`
exited 0. That covers the source, the examples, and the test layer under
`tsconfig.test.json` (debt ledger held, no growth).
- **Build and published surface:** `pnpm --filter
@objectstack/plugin-auth build` exited 0. The built `dist/*.d.ts` has 0
hits for `clientRegistrationDefaultApplicationType`, while
`dist/index.mjs` has 1 (positive control), so no published type moves.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 70 commands at `6d4fd2a5a` (merge
base `9cfc57533`). All 70 ran and exited 0, with each code captured
before any pipe. The `--ran` reconciliation 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 `6329352251` item 5 (carried from `6327761812`) 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
- The comment above `rawApp.post(... /sys-oauth-application/register
...)` in `auth-plugin.ts` still says DCR on `/oauth2/register` is
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.
- The `describe` titles in `auth-manager.mcp-oauth-resource.test.ts`
still 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](https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2a2f5c2 commit 6b44efa
3 files changed
Lines changed: 405 additions & 0 deletions
File tree
- .changeset
- packages/plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
0 commit comments