Repository navigation
Commit 950f949
fix(plugin-auth): a refused OAuth application registration answers the status the refusal declared, not 500 (#24853)
Part of #24773 — this PR does the first half only: a refused
registration now answers 4xx instead of 500. The second half, whether a
`web` client may register an `http://localhost` / `http://127.0.0.1`
redirect URI, is an authentication-flow acceptance rule. It was measured
here and nothing about it changed. It goes to a decision card. #24773
remains open.
Clause-②: no
## What changed
`POST /api/v1/auth/sys-oauth-application/register`
(`packages/plugins/plugin-auth/src/auth-plugin.ts`) answered most
refusals from better-auth as `500 OAUTH_REGISTER_FAILED`.
- **Mechanism, measured against the installed
`@better-auth/oauth-provider@1.7.3` / `better-call@1.4.0`.** Every
refusal better-auth raises is better-call's `APIError`. It carries its
status twice: as the number `statusCode` (`400`) and as the name
`status` (`'BAD_REQUEST'`). The door read only a numeric `status`, so
every refusal with a named status fell through to `500`. The body-schema
`ValidationError` has the number `400` in its `status`, so that refusal
answered `400` from the same door. One door gave two answers for the
same class of refusal.
- **Fix.** The door now reads the status through
`resolveThrownHttpError` (`@objectstack/types`). That is the repo's one
rule for which status a thrown error declares: a numeric `status`, then
`statusCode`, and the caller's fallback (`500`) when the throw declares
neither. The code stays `OAUTH_REGISTER_FAILED` (already in the ledger;
no new code). `details.upstreamError` still carries the vendor's
discriminator.
- **Effect, measured on the real stack.** `web` +
`http://localhost:4001/callback` or `http://127.0.0.1:4001/callback`:
`500` → `400` (`invalid_redirect_uri`). No session: `500` → `401`. A
storage fault while persisting the client: still `500`. Body-schema
refusal (`http://app.example.com/callback`): still `400`.
`https://app.example.com/callback`: still `200`.
## Tests
New
`packages/plugins/plugin-auth/src/oauth-register-refusal-status.test.ts`.
It runs real better-auth with the OIDC provider on, behind the plugin's
real route registration on a real Hono app, using the
`in-process-session-renewal.pin.test.ts` harness. Each refusal asserts
the ADR-0112 envelope (`code` + `status`) and `details.upstreamError`
where the vendor sends one, and asserts that no `sys_oauth_application`
row was written.
- A precondition asserts that the fixture's refusal is a
**named-status** `APIError` (`status: 'BAD_REQUEST'`, `statusCode:
400`). If the vendor ever switched to a numeric `status`, the pins below
would pass on the old door too, and this precondition says so.
- **Ablation, leg 1** (put the old read back: `const status = typeof
err?.status === 'number' ? err.status : 500;`). The mutation was applied
with `scripts/ablation-replace.mjs`, which confirmed it on disk (anchor
1 → 0, replacement 0 → 1, blob `03e026dea` → `a6bb1a7f3`). Result:
`Tests 3 failed | 14 passed (17)`. The failures were `expected 500 to be
400` (localhost), `expected 500 to be 400` (127.0.0.1) and `expected 500
to be 401` (no session). Restored: blob equal to HEAD, `git diff HEAD`
empty.
- **Ablation, leg 2** (unknown throw falls back to 400:
`resolveThrownHttpError(err, 400)`). Disk-verified the same way (blob
`03e026dea` → `16aa6cab0`). Result: `Tests 1 failed | 16 passed (17)`
with `expected 400 to be 500` on the storage-fault case. Restored the
same way.
- Both legs went red in the direction predicted and nothing else moved.
Both ran on head `5dc8add4f`, the commit that carries the fix. The merge
of `main` that followed left both files byte-identical (blobs
`03e026dea` and `8b2336493`).
**Head `622e30aa9`** is the merge of `main` at `5cb6e6e0a`. The incoming
commits touch none of this diff's files, and `packages/spec`
`check:generated` is all up to date after the rebuild. On that head:
- `pnpm --filter @objectstack/plugin-auth exec vitest run`: `Test Files
169 passed (169)`, `Tests 3241 passed | 10 skipped (3251)`.
- `pnpm --filter @objectstack/plugin-auth typecheck`: exit 0, with
`check:test-typecheck: OK`.
- The derived local gate union (`node scripts/pm/dispatch-gates.mjs
--commands`, 72 commands) ran with every exit code captured before any
pipe: 72 of 72 exit 0. The `--ran` reconciliation printed `72 derived,
72 run, 0 NOT-MEASURED, 0 UNRUN`.
## Half 2 — measured, not changed (for the decision card)
- **The vendor's predicate.** This is `validateClientRedirectUri`,
`@better-auth/oauth-provider@1.7.3`. A `web` client is refused unless
the URI is `https:` **and** the host is not loopback (`localhost` or a
loopback IP). The message "web clients require https redirect URIs on
non-loopback hosts" states that conjunction. It does not exempt loopback
hosts. Measured through this door, every loopback form is refused for
`web`: `http://localhost`, `http://localhost:4001`,
`http://127.0.0.1:4001`, `http://[::1]:4001`, and also
`https://localhost` and `https://127.0.0.1`. Port and IP family make no
difference. A `native` client may use `http` on exactly `localhost`,
`127.0.0.1` or `[::1]`, with any port. Called directly with
`application_type: 'native'`, the vendor accepted
`http://localhost:4001/callback` and `http://127.0.0.1:4001/callback`.
The predicate takes no options, so no configuration plugin-auth passes
(trusted origins, dev mode, dynamic client registration) governs it.
- **The door drops the Console's Application Type.** The form's `type`
select (web / native / user-agent-based) is forwarded as the body key
`type`. The installed vendor's `/oauth2/create-client` body schema
declares no `type`. It declares `application_type: 'web' | 'native'`. So
the key is stripped, and every registration is judged and stored as
`web`. Measured: `type: 'native'` + `http://127.0.0.1:4001/callback` is
refused with the web rule. `type: 'native'` + an `https` URI is stored
with `application_type: "web"`.
- **The field help.** "One URL per line. Must use https:// in
production." (`packages/platform-objects`, `sys_oauth_application`
create action) does not match the measured rule for `web`: https always,
never a loopback host.
The options, analysed on the four axes, are in the dev report on the
card. This PR changes none of them.
## Acceptance notes
- Not changed here, and noted without a card: on a server fault the door
answers with the thrown error's raw `message` (in the test, `storage
fault (test)`) and logs nothing on that branch. This is the existing
behaviour and out of this card's scope.
- `test:repo` (vitest `repo` project) was not run locally; it is CI's
(`Test Core`).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent df8bb40 commit 950f949
3 files changed
Lines changed: 227 additions & 2 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| |||
3624 | 3624 | | |
3625 | 3625 | | |
3626 | 3626 | | |
3627 | | - | |
| 3627 | + | |
| 3628 | + | |
| 3629 | + | |
| 3630 | + | |
| 3631 | + | |
| 3632 | + | |
| 3633 | + | |
| 3634 | + | |
| 3635 | + | |
| 3636 | + | |
| 3637 | + | |
3628 | 3638 | | |
3629 | 3639 | | |
3630 | 3640 | | |
| |||
| 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 | + | |
0 commit comments