Repository navigation
Commit 2941a19
fix(plugin-auth): every in-process auth bridge hands handleRequest the request's transport (#24931)
Fixes #24877
Clause-②: no
## What changes
`AuthManager.handleRequest(request, transport?)` establishes a request's
client address by the platform's client-address trust rule (ADR-0069
D5/D7, `resolveClientAddress`, `server.trustProxy`): forwarded headers
only when the deployment declares a trusted proxy, the transport's peer
address otherwise. Every in-process bridge in `auth-plugin.ts` that
re-dispatches through `handleRequest` now judges the re-dispatched
request on the same facts as the request it serves, on both branches of
that rule:
- **The transport.** Each bridge hands `handleRequest` the transport of
the request it serves, as the catch-all auth mount does
(`transportOf(c)`).
- **The forwarding headers.** The six bridges whose helper rebuilds the
request before re-dispatching it now carry the incoming request's
forwarding headers onto it. The list is the rule's own,
`FORWARDED_CLIENT_ADDRESS_HEADERS`. `resolveClientAddress` now iterates
that same list, and the helpers copy it through one shared function, so
no helper names a header itself. Without a declared trusted proxy, the
rule ignores those headers and the re-dispatched request is judged on
the connection peer. With one, it is judged on the forwarded client
address.
| Bridge | How it reaches `handleRequest` | Transport | Forwarding
headers |
|---|---|---|---|
| `POST /admin/remove-user` | delegates the request itself |
`handleRequest(c.req.raw, transportOf(c))` | already on the request |
| `POST /admin/has-permission` (delegate lane) | delegates the request
itself | `handleRequest(c.req.raw, transportOf(c))` | already on the
request |
| `POST /admin/sso/register` | `runRegisterSsoProviderFromForm(handle,
…)` | `handle` is `handleWithTransportOf(c)` | `forwardAuthHeaders` (now
also used here), and the session read |
| `POST /admin/sso/register-saml` |
`runRegisterSamlProviderFromForm(handle, …)` | `handle` is
`handleWithTransportOf(c)` | `forwardAuthHeaders` (now also used here),
and the session read |
| `POST /admin/sso/request-domain-verification` |
`runRequestDomainVerification(handle, …)` | `handle` is
`handleWithTransportOf(c)` | `forwardAuthHeaders` |
| `POST /admin/sso/verify-domain` | `runVerifyDomain(handle, …)` |
`handle` is `handleWithTransportOf(c)` | `forwardAuthHeaders` |
| `POST /send-verification-email` | `runResendVerificationEmail(handle,
…)` | `handle` is `handleWithTransportOf(c)` | the session read and the
send |
| `POST /sys-session/revoke` | `sysSessionRevokeDepsFor(manager, …)`,
whose `handle` calls `manager.handleRequest` | the manager slice's
`handleRequest` is `handleWithTransportOf(c)` | the owner lane's
re-dispatch |
`handleWithTransportOf(c)` is one helper beside `gateAdmin`: `(request)
=> this.authManager!.handleRequest(request, transportOf(c))`. The list
and the one copy function, `carryForwardedClientAddress(from, to)`, live
in a new internal module, `client-address-forwarding.ts`. That module is
import-free and is not on the package's `exports` map. Neither the
package root nor the published `./client-address` entry re-exports it,
so this PR adds no published export: the built `./client-address` entry
still exports `resolveClientAddress` alone. `client-address.ts` imports
the list for `resolveClientAddress`. The three helper modules call the
shared function: twice in `register-sso-provider.ts`
(`forwardAuthHeaders` and the session read), twice in
`send-verification-email.ts` (the session read and the send), and once
in `sys-session-revoke.ts` (the owner lane). The OIDC and SAML register
builders were byte-identical to `forwardAuthHeaders` and now call it.
`transportOf` and `handleRequest` are unchanged. `resolveClientAddress`
decides exactly as before: it reads `X-Forwarded-For`, then `X-Real-IP`,
now from the list rather than from two literals.
### The eighth bridge
The card lists seven sites, from `git grep -n "handleRequest(" --
packages/plugins/plugin-auth/src/auth-plugin.ts`. `POST
/sys-session/revoke` is an eighth. Its owner lane re-dispatches through
`handleRequest` one hop away, inside `sysSessionRevokeDepsFor`
(`sys-session-revoke.ts`: `handle: (request) =>
manager.handleRequest(request)`), so that grep does not list it. It is
the same rule and the same mechanical fix, so it is fixed here. The
mount hands the factory a manager slice whose `handleRequest` carries
the transport. The seat accepted it and amended the claim's surface.
## Pins
`packages/plugins/plugin-auth/src/bridge-redispatch-transport.pin.test.ts`
uses the D5/D7 real-mount fixture from
`client-address-vendor-trust-rule.test.ts`:
`AuthPlugin.registerAuthRoutes` on a real Hono app in front of a real
`AuthManager` on the installed better-auth, with the peer on
`c.env.incoming`. It runs two deployments: one declaring no trusted
proxy, one declaring `trustProxy`. Each pin reads the client address
carried by every request the bridge hands the vendored library's
handler. That address is the platform-owned header `handleRequest`
writes from the address it established. The bridge's own inner route
must be among those requests, so a bridge that never reached the library
cannot pass vacuously. Every request also carries the platform-owned
header naming another address, and on neither branch may it decide
anything.
No trusted proxy declared (the caller's forwarding headers name another
address):
- control: a request through the catch-all is judged on its connection
peer
- a request through ROUTE is judged on its connection peer when no
trusted proxy is declared, once for each of the eight bridges
A trusted proxy declared (the connection peer is the proxy hop; each pin
sends `X-Forwarded-For` and, separately, `X-Real-IP`):
- control: a request through the catch-all is judged on the forwarded
client address
- a request through ROUTE is judged on the forwarded client address when
a trusted proxy is declared, once for each of the eight bridges
## Ablation
Each leg ran through `scripts/ablation-replace.mjs` with the anchor hit
count declared and the mutation confirmed on disk. Legs A to C ran at
`486e46d1b`; `auth-plugin.ts` and the pin file are byte-identical at
this PR's head. Legs D to G ran at this PR's head, `fbd92ad31`. Each
restore was proven: blob equals HEAD's, `git diff HEAD` is empty, and
the tree is clean. The test imports its subjects from `src/`, so no
`dist/` sits on the path.
| Leg | Forbidden behaviour put back | Red (`Tests N failed \| M passed
(28)`) |
|---|---|---|
| A | `handleWithTransportOf` drops the transport | 6: the no-trust pins
of sso/register, sso/register-saml, request-domain-verification,
verify-domain, send-verification-email and sys-session/revoke |
| B | the two delegate sites drop `transportOf(c)` (2 hits) | 2: the
no-trust pins of remove-user and has-permission |
| C | the revoke mount goes back to
`sysSessionRevokeDepsFor(this.authManager!, basePath)` | 1: the no-trust
pin of sys-session/revoke |
| D | `register-sso-provider.ts` stops calling the shared copy (2 calls)
| 4: the trusted-proxy pins of sso/register, sso/register-saml,
request-domain-verification and verify-domain |
| E | `send-verification-email.ts` stops calling it (2 calls) | 1: the
trusted-proxy pin of send-verification-email |
| F | `sys-session-revoke.ts` stops calling it (1 call) | 1: the
trusted-proxy pin of sys-session/revoke |
| G | the shared `carryForwardedClientAddress` copies nothing | 6: the
trusted-proxy pins of the six helper bridges |
Every other pin, and both controls, stayed green in every leg.
## Verification
All readings at `fbd92ad31`: this branch with `origin/main` `74fcec1ed`
merged. No later `main` commit touches `plugin-auth` or the lockfile.
- `pnpm --filter '@objectstack/plugin-auth...' build`, then `pnpm
--filter @objectstack/plugin-auth run typecheck`: exit 0. That covers
`tsc --noEmit`, the examples config and `check:test-typecheck: OK`,
whose debt ledger is unchanged at 10 files / 88 errors / 23 signatures.
The pin file is inside that program and has no entry in the ledger.
- The built entries were read back. `dist/client-address.js` and
`dist/client-address.mjs` export `resolveClientAddress` alone and
require nothing. The package root exports neither the list nor the copy.
Spot checks of `resolveClientAddress` gave the same answers as before:
the chain's first entry, then `X-Real-IP`, the peer when trust is
undeclared, and `undefined` when neither is present.
- `pnpm --filter @objectstack/plugin-auth exec vitest run
--maxWorkers=2`: `Test Files 172 passed (172)`, `Tests 3309 passed | 10
skipped (3319)`.
- `node scripts/pm/dispatch-gates.mjs --commands` with no paths printed
72 commands. Each ran with its exit code captured before any pipe. 71
exited 0 on the first pass. `check:dual-build-cjs-loads` exited 3
(`PREREQUISITE NOT MET`: no workspace `dist/` yet in this fresh
worktree). After a later gate in the same pass had built the workspace,
it re-ran and exited 0. `--ran` reconciled them: `72 derived famil(ies)
accounted for — 72 run, 0 NOT-MEASURED`.
- `check:published-readme-exports` exited 0, with 0 entries in its
baseline. The roster gates under touched directories also exited 0:
`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing` and `check:filter-alias-parity`. eslint
(`--no-inline-config`) on the seven changed source files exited 0.
- CI on `fbd92ad31`: 41 check-runs, 34 success and 7 skipped, among them
`Lint & Repo Gates`, `TypeScript Type Check`, `Test Core`, `Build Core`
and `Governed Surface Queue Guard`.
## Acceptance notes
- `sysSessionRevokeDepsFor` could take the transport itself. That edit
lands in the factory's signature, outside this card's surface. The slice
keeps the transport change in `auth-plugin.ts`.
- `IAuthService.handleRequest(request)` in the spec contract has no
transport parameter, and two callers outside `plugin-auth` hand the
request alone. Their reach is unmeasured, and both sit in landings this
card excludes. Carrier: none.
- No new gate. The pins sit beside the code, and a bridge added later is
covered only when its PR pins it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 19e5338 commit 2941a19
8 files changed
Lines changed: 465 additions & 28 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 | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
72 | | - | |
| 72 | + | |
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| |||
2960 | 2960 | | |
2961 | 2961 | | |
2962 | 2962 | | |
| 2963 | + | |
| 2964 | + | |
| 2965 | + | |
| 2966 | + | |
| 2967 | + | |
| 2968 | + | |
| 2969 | + | |
| 2970 | + | |
| 2971 | + | |
| 2972 | + | |
| 2973 | + | |
| 2974 | + | |
| 2975 | + | |
| 2976 | + | |
| 2977 | + | |
2963 | 2978 | | |
2964 | 2979 | | |
2965 | 2980 | | |
| |||
2993 | 3008 | | |
2994 | 3009 | | |
2995 | 3010 | | |
2996 | | - | |
| 3011 | + | |
2997 | 3012 | | |
2998 | 3013 | | |
2999 | 3014 | | |
| |||
3293 | 3308 | | |
3294 | 3309 | | |
3295 | 3310 | | |
3296 | | - | |
| 3311 | + | |
3297 | 3312 | | |
3298 | 3313 | | |
3299 | 3314 | | |
| |||
3370 | 3385 | | |
3371 | 3386 | | |
3372 | 3387 | | |
3373 | | - | |
| 3388 | + | |
3374 | 3389 | | |
3375 | 3390 | | |
3376 | 3391 | | |
| |||
3493 | 3508 | | |
3494 | 3509 | | |
3495 | 3510 | | |
3496 | | - | |
| 3511 | + | |
3497 | 3512 | | |
3498 | 3513 | | |
3499 | 3514 | | |
| |||
3521 | 3536 | | |
3522 | 3537 | | |
3523 | 3538 | | |
3524 | | - | |
| 3539 | + | |
3525 | 3540 | | |
3526 | 3541 | | |
3527 | 3542 | | |
| |||
3539 | 3554 | | |
3540 | 3555 | | |
3541 | 3556 | | |
3542 | | - | |
| 3557 | + | |
3543 | 3558 | | |
3544 | 3559 | | |
3545 | 3560 | | |
| |||
3671 | 3686 | | |
3672 | 3687 | | |
3673 | 3688 | | |
| 3689 | + | |
| 3690 | + | |
| 3691 | + | |
| 3692 | + | |
| 3693 | + | |
| 3694 | + | |
3674 | 3695 | | |
3675 | 3696 | | |
| 3697 | + | |
3676 | 3698 | | |
3677 | | - | |
| 3699 | + | |
| 3700 | + | |
| 3701 | + | |
| 3702 | + | |
| 3703 | + | |
| 3704 | + | |
| 3705 | + | |
| 3706 | + | |
| 3707 | + | |
3678 | 3708 | | |
3679 | 3709 | | |
3680 | 3710 | | |
| |||
3702 | 3732 | | |
3703 | 3733 | | |
3704 | 3734 | | |
3705 | | - | |
| 3735 | + | |
3706 | 3736 | | |
3707 | 3737 | | |
3708 | 3738 | | |
| |||
0 commit comments