Repository navigation
Feat/desktop oauth pkce - #250
Merged
Merged
Conversation
A downloaded desktop app is a public OAuth client — it ships an embedded client_id and can't protect a confidential secret. Add PKCE (RFC 7636) to all six Google connect/callback pairs (Drive, Gmail, Calendar, Contacts, Sheets, Forms): generate a verifier, put the S256 challenge on the auth URL, carry the verifier in the existing oauth_states metadata (no schema change), and replay it at token exchange. Gated behind OAUTH_PUBLIC_CLIENT, baked to "true" in workerd.desktop.capnp and left unset in cloud — so the cloud confidential (client_secret) flow is unchanged. Shared helpers usePublicClient/pkceChallengeForState/ applyPkceVerifier keep the per-provider edits minimal. Next: GitHub device flow; bake the real Desktop client_id into the build. REVISION: oauth-pkce-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
…ld time A downloaded app can't read host env vars, so provider client IDs/secrets must ship inside the app. When GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET (etc.) are set at build time, rewrite the matching `fromEnvironment` bindings to literal `text` values in the STAGED capnp only — source stays clean, so nothing lands in the public repo (which also keeps Google from auto-revoking an exposed secret). No-op when the vars are unset. GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=... cargo tauri build Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
GitHub OAuth has no PKCE and its client secret is genuinely confidential, so a downloaded app can't use the redirect+secret flow. Add the OAuth device flow (RFC 8628): POST github/device/start returns a user_code + verification_uri (device_code stashed under a state handle), and github/device/poll exchanges grant_type=device_code, returning pending / slow_down / complete / error and persisting the token to user_integrations exactly like the redirect callback. start is gated to the public-client build; cloud keeps the redirect flow. Needs only GITHUB_CLIENT_ID (no secret). REVISION: github-device-flow-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
Primary fix: the workerd `internet` network service had no tlsOptions, so it was HTTP-only — every server-side fetch to an https:// URL failed with "this HttpClient doesn't support HTTPS". That silently broke ALL outbound integration calls on desktop (OAuth token exchange, GitHub device flow, Gmail/Drive APIs, Resend). Add `tlsOptions = (trustBrowserCas = true)`. Hardening (surfaced while reviewing the egress implications): - Drop "private" from the network allow-list (keep public + local): the control plane only reaches loopback (d1-shim, sandbox) + public APIs, so a control-plane SSRF can no longer reach private-LAN hosts. - /embed-check (a server-side fetch of a caller-supplied URL) now rejects PAT-authed callers via rejectPatAuth, alongside its existing private-hostname block. Note: this does NOT widen the sandbox egress monitor (that governs VM PTY traffic via iptables; the control plane was never in its path). REVISION: desktop-workerd-tls-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
Closes the desktop trust-boundary gap: dev-auth is header-spoofable and a process inside the sandbox VM can reach the control plane at :8787 (the guest->host bridge; localhost bypasses the egress proxy), so it could impersonate the user against user-scoped endpoints. Fix: the desktop app generates a per-boot SURFACE_TOKEN, passes it to the control-plane worker, and exposes it to the GUI webview via a new get_surface_token Tauri command. The frontend sends it as X-Orcabot-Surface on control-plane calls (fetched+cached in tauri-bridge, ensured before the first authed request), and the control plane only honors dev-auth when the header matches (devAuthSurfaceTrusted). The VM never gets the token and can't reach the frontend (:8788) to scrape it, so it's confined to the token-authed paths it should use (gateway PTY token, /internal token). When SURFACE_TOKEN is unset (cloud, local dev, older builds) enforcement is off — behavior unchanged. Desktop-only; prod runs DEV_AUTH_ENABLED=false. REVISION: surface-token-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
… boundary The surface-token gate would also 401 the trusted host clients that use dev-auth (the orcabot CLI, scripts), not just the VM. Fix: orcabot-desktop also writes the token to a host-only file (data_dir/surface-token, 0600). The app-data dir isn't shared into the sandbox VM (only /workspace is), so a VM process still can't read it, but the CLI can — apply_auth now sends X-Orcabot-Surface for local targets. Also document the trust boundary in desktop/CLAUDE.md. REVISION: surface-token-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
Proves the trust boundary: dev-auth + valid surface token -> 200, but dev headers with NO token (the VM-agent case) or a WRONG token -> 401. Reads the host-only token file; SKIPs cleanly on builds that predate the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
…rebuild The dev orcabot-desktop binary loads resources from target/release/resources (populated by cargo build), but build-desktop-resources.sh only wrote to the source app/src-tauri/resources — so a standalone resources rebuild silently had no effect until a full cargo build. Sync the two at the end (only when the target dir exists), so `build-desktop-resources.sh` + `orcabot up` takes effect. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
On desktop (public OAuth client), the workspace GitHub connect button now opens a device-code dialog instead of the redirect popup: it calls github/device/start, shows the user_code + a button to github.com/login/ device, polls github/device/poll, and on complete refreshes the GitHub integration + opens the repo picker (same as the redirect callback did). Cloud keeps the popup redirect flow. REVISION: github-device-dialog-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
providers.tsx establishes the desktop user with raw fetch() calls (/auth/dev/session + /me ID-sync) using getAuthHeaders() — but the surface token is only loaded inside apiFetch, so those bootstrap calls carried no X-Orcabot-Surface and 401'd under enforcement. The /me ID-sync then failed, leaving the local user on a client-generated ID (wrong user → empty dashboards). Await ensureSurfaceToken() before the bootstrap fetches so they carry the token. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
…trap The real cause of "Failed to load dashboards": the frontend adds the X-Orcabot-Surface header, but it wasn't in the control plane's CORS Access-Control-Allow-Headers, so the browser preflight blocked every authed request before it reached the server. Add X-Orcabot-Surface to CORS_ALLOWED_HEADERS. Also: providers.tsx now sets the local dev user before awaiting the surface token (bounded) so a slow token fetch can't block login, and reuses userId for the ID-match check. Reverted the temporary diagnostic logs. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
…) + Gmail window.open is a no-op in the Tauri webview, so every popup-based OAuth connect button silently did nothing on desktop. Add an open_url Tauri command (opens the OS default browser) + a connectViaBrowser helper that opens the provider URL there and polls the integration status until connected (replacing the popup + postMessage handshake, which can't work across the OS browser). Wire Gmail as the reference; other providers follow. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
Wire connectViaBrowser (open OS browser + poll status) into every provider that used a window.open popup, which is a no-op in the Tauri webview: Google Calendar/Contacts/Sheets/Forms, Drive/Box/OneDrive (WorkspaceBlock), Microsoft Outlook/Outlook-Calendar/Teams, Slack, Discord, and the generic IntegrationsPanel. Each keeps its existing web popup/postMessage path; the desktop branch opens the OS browser and refreshes the block on connect. GitHub (device-flow modal) and Gmail (reference) were done earlier. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
…owser OAuth The connect endpoints require auth, but a top-level browser navigation (the OAuth connect opened in the OS browser) can't send X-User-ID or X-Orcabot-Surface headers, so it 401'd. connectViaBrowser now appends user_id/user_email/user_name + surface as query params, and the surface gate accepts the token via ?surface= too (authenticateDevMode already reads user_id from the query, as it does for WebSockets). Header path unchanged for fetch/XHR. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
connectViaBrowser now re-checks the integration status immediately when the app regains focus (window focus + visibilitychange), not just on the setInterval — background webviews throttle timers while the OS browser is foreground, so returning to Orcabot now picks up the connection right away instead of only on the next click. Also: the OAuth success page's Close-tab button falls back to a "close it yourself" hint when the browser blocks window.close() (e.g. Safari). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
… more providers The webview's own focus/visibilitychange events don't fire reliably across an OS app-switch (Safari -> Orcabot), so returning after authorizing didn't pick up the connection until the next click. connectViaBrowser now also subscribes to the native `tauri://focus` event and re-checks the status when the app becomes active again. Also extend the build-time OAuth-credential bake list to Microsoft, Slack, Discord, Twitter, OneDrive, and Box so those providers can be configured the same way as Google/GitHub. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
They rely on confidential OAuth client secrets that the desktop build doesn't ship, so drop them from the messaging integrations palette when DESKTOP_MODE (cloud keeps them). Removes dead-end connect buttons on desktop. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
…s/OneDrive) Same treatment as Google, plus: Microsoft public clients send no secret, so the connect/callback drop client_secret and relax the "not configured" guard on the public-client path (only client_id required). Cloud keeps the confidential Web-app flow. Token-refresh path still to convert. REVISION: oauth-ms-pkce-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
The Microsoft refreshers (refreshOnedriveAccessToken, refreshOutlookAccessToken, and the gateway token-refresh.ts branch that covers teams/outlook/onedrive/ outlook_calendar) sent client_secret unconditionally and required it, so a public-client integration would break at the ~1h token expiry. Relax the guard and omit client_secret when OAUTH_PUBLIC_CLIENT is set, so PKCE-issued tokens refresh with no secret. Cloud (confidential) path unchanged. REVISION: oauth-ms-pkce-v1 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
WhatsApp needs the Baileys bridge (a separate Fly service) the desktop build doesn't bundle, so hide it on desktop alongside Slack/Discord/X. With all messaging providers hidden on desktop, also hide the Messaging toolbar section when it's empty so it doesn't render a dangling collapse toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
…ce dialog; hide Box P1a: restrict the build-time bake list to non-confidential values only — public client IDs + GOOGLE_CLIENT_SECRET (Google's non-confidential desktop secret) + GOOGLE_API_KEY. GitHub (device flow) and Microsoft/OneDrive (PKCE) need no secret; confidential-secret providers (Slack/Discord/X/Box) are never baked. P2: GithubDeviceDialog no longer restarts the device flow on every parent re-render (onConnected/onOpenChange moved to refs so `begin` is stable), and the "open github.com/login/device" button uses openExternalUrl instead of window.open (a no-op in the Tauri webview). Box: hidden on desktop (confidential secret, can't go secretless like MS). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_014t8Ukp4NPWtqJ55X261wMZ
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.
No description provided.