fix(auth): make sign-in work across proxy configurations, desktop and local dev#408
Merged
Conversation
…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>
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:signInWith()used undici, which consults no proxy; the direct connection tooauth2.googleapis.comtimes out.403 nonce replay. Bonus finding from the same incident: a local proxy accepts CONNECTs to localhost and hangs them.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 usefetchOnce.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 egressDEV_GOOGLE_TOKEN_URL— the web flow's token exchange relays through a dev-only vite middleware on share-web (/__dev/google-token, Node undiciEnvHttpProxyAgent), added only when a proxy env is presentCoverage matrix (validated live)
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 classificationoauth.test.ts4/4 (electron mock providesnet.fetch;session.fromPartitionstub throws if a fallback engages unexpectedly)setDevJwkscases (verifies with injected keys + zero network fetcher calls; malformed JSON throws)share-publish.spec.tse2e 15/15🤖 Generated with Claude Code