Skip to content

fix(auth): make sign-in work across proxy configurations, desktop and local dev#408

Merged
graydawnc merged 4 commits into
mainfrom
fix/oauth-system-proxy
Jul 8, 2026
Merged

fix(auth): make sign-in work across proxy configurations, desktop and local dev#408
graydawnc merged 4 commits into
mainfrom
fix/oauth-system-proxy

Conversation

@graydawnc

@graydawnc graydawnc commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

What

Sign-in failed for users behind a proxy — share-auth:signin: TypeError: fetch failed, later plain 30s timeouts. Field-testing on a proxy-only network (no TUN) peeled three distinct layers, each real:

  1. Desktop transportsignInWith() used undici, which consults no proxy; the direct connection to oauth2.googleapis.com times out.
  2. Blind transport retry double-spent one-shot values — the sign-in POST reached the backend through the system proxy (consuming the anti-replay nonce), the proxy swallowed the response, and the fallback transport re-sent the same id_token → 403 nonce replay. Bonus finding from the same incident: a local proxy accepts CONNECTs to localhost and hangs them.
  3. The local backend itself must reach Google — JWKS for id_token verification (both flows) and the code→token exchange (web flow) run inside workerd, whose outbound fetch consults no proxy (workers-sdk#4515, backlog). Production is unaffected — the deployed backend runs on Cloudflare's network.

How

main/net/robust-fetch.ts — transport chain through Chromium's stack (no new dependency): net.fetch (OS proxy) → env-proxy session → forced-direct session. Loopback targets always bypass proxies. Replayable requests (robustFetch) fall through on connect-level failures only; one-shot requests (fetchOnce) pick a transport with a side-effect-free GET probe, then send the real request exactly once — never retried across transports. Both OAuth calls use fetchOnce.

Local-dev backend reroutes (standard emulator move — inject what the sandbox can't reach; active only via share-dev.sh bindings, prod never sets them):

  • DEV_JWKS — share-dev.sh pre-fetches the public signing keys on the host (curl follows the shell proxy); setDevJwks() verifies with zero worker egress
  • DEV_GOOGLE_TOKEN_URL — the web flow's token exchange relays through a dev-only vite middleware on share-web (/__dev/google-token, Node undici EnvHttpProxyAgent), added only when a proxy env is present

Coverage matrix (validated live)

Scenario Desktop Web (local dev)
Direct connectivity, no proxy net.fetch direct unchanged (no bindings)
System/env proxy, no TUN probe picks proxy transport; backend calls stay direct-loopback JWKS injected + token relay
TUN mode any transport any path

Test plan

  • robust-fetch.test.ts (17): transport fall-through, 4xx no-retry, probe-then-commit single-send, commit failure not retried, loopback forced direct, env parsing, error classification
  • oauth.test.ts 4/4 (electron mock provides net.fetch; session.fromPartition stub throws if a fallback engages unexpectedly)
  • share-backend 247/247 incl. new setDevJwks cases (verifies with injected keys + zero network fetcher calls; malformed JSON throws)
  • Full suites green in final state: app 476, share-web 84, share-publish.spec.ts e2e 15/15
  • Live validation on a proxy-only network (TUN off): desktop sign-in ✅, web sign-in ✅; fake-code probes returned Google 400 / backend 403 in <1s at every layer

🤖 Generated with Claude Code

graydawnc and others added 2 commits July 8, 2026 01:03
…port

signInWith() used the global fetch (undici in the main process), which
ignores the OS proxy configuration. Behind a system proxy the direct
connection to oauth2.googleapis.com times out (UND_ERR_CONNECT_TIMEOUT)
and sign-in fails with "share-auth:signin: TypeError: fetch failed".

Switch the token exchange and the backend sign-in POST to Electron's
net.fetch, which goes through the Chromium network stack and honours
the OS proxy and trust store — the same fix authedFetch (share/
api-client.ts) already carries for every other backend call.

Known limitation: with one tested proxy setup (mihomo HTTP proxy on
127.0.0.1 set as the macOS system proxy) sign-in still required the
proxy's TUN mode; net.fetch's proxy resolution there needs a separate
look. This change is still strictly better than undici, which never
consults the proxy at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…here

net.fetch alone is not enough: a stale system-proxy config takes
working direct connectivity down with it, dev-shell https_proxy vars
are consulted by nothing, and one observed loopback-proxy setup fails
through net.fetch while the proxy itself is healthy.

Add robustFetch: try net.fetch (OS proxy / direct), then a session
pinned to the env-var proxy when one is set, then a forced-direct
session — all through Chromium's network stack, no new dependency.
Only connect-level failures fall through; HTTP responses (including
4xx) resolve normally, so the single-use OAuth code can't be
double-spent across transports. Both OAuth calls now use the chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@graydawnc graydawnc changed the title fix(auth): route desktop OAuth through net.fetch for system-proxy support fix(auth): make desktop sign-in work across proxy configurations Jul 7, 2026
graydawnc and others added 2 commits July 8, 2026 11:01
… transports

Field testing caught the flaw in blind transport fallback: the sign-in
POST reached the local backend through the system proxy (consuming the
anti-replay nonce), the proxy sat on the response until the timeout,
and the retry on the next transport re-sent the same id_token — 403
nonce replay. "Delivered but response lost" is indistinguishable from
"never delivered" at the connect level.

fetchOnce() picks a transport with a side-effect-free GET probe of the
target origin (any HTTP status proves reachability), then sends the
real request exactly once with a generous timeout; failures after the
commit propagate instead of retrying. Both OAuth POSTs use it.

Same incident, second lesson: loopback targets now always bypass the
proxy transports — a local proxy accepted the CONNECT to localhost and
hung, which is also what broke sign-in against the dev backend under a
plain system proxy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etch

Even with the desktop transport chain fixed, local sign-in still hung:
the backend itself must reach Google — JWKS for id_token verification
on both flows, plus the code→token exchange on the web flow — and
workerd's outbound fetch consults no proxy (cloudflare/workers-sdk
issue 4515, backlog). On proxy-only dev networks both calls hang until
the client gives up. Production is unaffected: the deployed backend
runs on Cloudflare's network.

Standard emulator move — inject what the sandbox can't reach:

- JWKS: share-dev.sh pre-fetches the (public) signing keys on the host
  (curl follows the shell proxy) and passes them via a DEV_JWKS
  binding; setDevJwks() then verifies without leaving the machine.
- Token exchange: can't be pre-fetched (dynamic POST), so when a proxy
  env is present share-dev.sh points the backend at a dev-only vite
  middleware on share-web (/__dev/google-token) that relays the POST
  through Node's proxy-aware undici (EnvHttpProxyAgent).

Both reroutes exist only when share-dev.sh injects the bindings; with
neither set the worker behaves exactly as before, and prod never sets
them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@graydawnc graydawnc changed the title fix(auth): make desktop sign-in work across proxy configurations fix(auth): make sign-in work across proxy configurations, desktop and local dev Jul 8, 2026
@graydawnc
graydawnc added this pull request to the merge queue Jul 8, 2026
Merged via the queue into main with commit 34e3b48 Jul 8, 2026
6 checks passed
@graydawnc
graydawnc deleted the fix/oauth-system-proxy branch July 8, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant