Skip to content

🤖 feat: Login with Coder from a browser on a remote Xum server - #4047

Merged
ThomasK33 merged 2 commits into
mainfrom
feat/coder-oauth-server-callback
Sep 2, 2026
Merged

🤖 feat: Login with Coder from a browser on a remote Xum server#4047
ThomasK33 merged 2 commits into
mainfrom
feat/coder-oauth-server-callback

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

"Login with Coder" now works from a browser connected to a Xum server, including a remote one. The authorization redirect lands on the Xum server's own /auth/coder/callback route instead of a desktop loopback listener, following the pattern the Gateway/MCP OAuth flows already use. The Settings UI no longer disables the login control on remote hosts.

Background

The Coder OAuth flow (authorization-code + PKCE, RFC 7591 dynamic client registration with exact redirect-URI matching) only had a desktop implementation: it bound an ephemeral 127.0.0.1:<port> listener and registered that as the client's redirect URI. A browser talking to a remote Xum server can never reach that listener, so ProvidersSection hid the control behind isRemoteServer with the message "Login with Coder requires the desktop app or a locally hosted Xum server".

That limitation was self-imposed. Coder's OAuth2 provider accepts non-loopback redirect URIs (HTTPS required for non-localhost hosts — verified against codersdk/oauth2_validation.go), and Xum already hosts server-side OAuth callbacks for other providers. Since DCR lets Xum register any redirect URI, the server can register its own public callback URL.

Implementation

  • coderOauthService.ts — the callback source is abstracted as a CoderCallbackChannel: the existing LoopbackServer, or a new ServerCallbackChannel (server: null) for browser mode. startServerFlow({ deploymentUrl, flowId, redirectUri }) runs the same launch pipeline as startDesktopFlow with the server channel; handleServerCallback({ state, code, error, errorDescription }) delivers the redirect to its flow (state = flow ID) and resolves with the login outcome for the route to render. Delivery is one-shot and raced against the flow's own completion so a Cancel/timeout mid-exchange settles the browser request instead of hanging it. Server flows live in the same OAuthFlowManager, so waitForDesktopFlow, cancelDesktopFlow, disconnect, the stored-client lease, the cross-process commit lock, and revocation apply unchanged.
  • orpc/server.tsGET /auth/coder/start?deploymentUrl&flowId (authenticated; the redirect URI is built server-side from the validated public host incl. app-proxy prefix and is never accepted from the client — there is deliberately no oRPC surface that takes a redirect URI) and ALL /auth/coder/callback (unauthenticated navigation, added to the origin-bypass set, GET + form_post). The byte-identical Gateway/MCP callback HTML is extracted into one sendOAuthCallbackPage helper rather than adding a fourth copy; Governor keeps its own template.
  • ProvidersSection.tsx — branches on isDesktop like the Gateway flow: desktop → oRPC loopback flow; browser (local or remote) → /auth/coder/start, then the existing oRPC wait/cancel. The remote-server paragraph and its gate on the palette hint are removed. The flow ID (which doubles as the OAuth state) is now derived from crypto.getRandomValues because crypto.randomUUID is undefined outside secure contexts (plain-HTTP remote origins), and "Copy & Open Coder" opens before the best-effort clipboard write for the same reason (navigator.clipboard is undefined there).
  • Docs (providers.mdx + regenerated built-in skill content) describe remote login and the HTTPS requirement.

Validation

Dogfooded end-to-end in a dev-server-sandbox served at a non-localhost hostname (xum.127.0.0.1.nip.io) against a mock Coder deployment implementing buildinfo, RFC 8414 discovery, DCR, an explicit Allow/Deny consent page, token exchange, revocation, and the AI Gateway listings:

  • DCR registered http://xum.127.0.0.1.nip.io:<port>/auth/coder/callback (the Xum server's origin, built from the request host); the authorize URL carried that redirect URI plus an S256 challenge.
  • Allow → redirect to /auth/coder/callback → PKCE exchange (verifier present) → catalog discovery → Settings shows Connected with Re-login / Refresh models / Disconnect; the callback tab auto-closed.
  • Deny → server-rendered "Login failed: access_denied: The user declined" page; Settings shows the inline error and the previous login stays connected.
  • Dogfooding surfaced the two insecure-context bugs above (randomUUID, navigator.clipboard), which would have silently broken login on plain-HTTP remote servers.

New tests: service (full server flow with exact HTTPS redirect URI in DCR + exchange + persistence + replay refusal; OAuth error redirect; cancel mid-exchange settles the pending callback and revokes; invalid redirect URI rejected pre-network), routes (auth required on start, app-proxy-aware redirect URI, callback success/failure rendering, cross-origin POST on the new callback), UI (browser-mode hint starts via /auth/coder/start and continues on waitForDesktopFlow; remote host renders the login control and targets its own origin).

Risks

Medium, scoped to the Coder provider login. The desktop path is only refactored to consume the channel interface (the loopback LoopbackServer is structurally compatible; all 88 pre-existing service tests pass unchanged). The new unauthenticated callback route only looks up a live flow by state and hands it an authorization code that is useless without this process's PKCE verifier and client secret. Gateway/MCP callback routes are refactored to the shared page helper with byte-identical output (existing route tests cover them).

Known limitation: Coder rejects http:// redirect URIs for non-localhost hosts, so a remote Xum server reachable only over plain HTTP gets a clear DCR error from the deployment rather than a login.


Generated with xum • Model: anthropic:claude-fable-5-1 • Thinking: xhigh • Cost: $29.72

@mintlify

mintlify Bot commented Sep 2, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Mux 🟢 Ready View Preview Sep 2, 2026, 12:07 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector

This comment has been minimized.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: ccc39d577f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

The Coder OAuth flow only supported a desktop loopback listener, so the
Settings UI disabled login when served from a remote Xum server. The
authorization redirect now lands on the server's own /auth/coder/callback
route in browser mode (like the Gateway/MCP server flows): the service
gains a server-hosted callback channel behind the existing flow manager,
the HTTP layer builds the redirect URI from the validated public host, and
the UI keeps waiting/cancelling through oRPC.
Dogfooding the browser flow from a non-localhost host surfaced two
insecure-context gaps: crypto.randomUUID is undefined there (the flow ID,
which doubles as the OAuth state, now comes from getRandomValues), and
navigator.clipboard is undefined too (open the authorization page before
the best-effort copy so a throw cannot swallow the navigation).
@ThomasK33
ThomasK33 force-pushed the feat/coder-oauth-server-callback branch from ccc39d5 to f89d97d Compare September 2, 2026 09:34
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: f89d97db4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

This comment has been minimized.

@ThomasK33
ThomasK33 added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit ede0574 Sep 2, 2026
20 of 21 checks passed
@ThomasK33
ThomasK33 deleted the feat/coder-oauth-server-callback branch September 2, 2026 09:59
asm pushed a commit to asm/mux that referenced this pull request Sep 2, 2026
## Summary

Version bump for the v0.28.4 patch release. The headline change since
v0.28.3 is Gemini 3.8 Flash becoming the default Gemini Flash model
(coder#4060). The release also carries browser Login with Coder on remote Xum
servers (coder#4047), the opt-in project bundle for settings backup (coder#4043),
the connection-indicator slow-response surfacing (coder#4059), send-queue and
terminal-wake fixes (coder#4053, coder#4052), and the Effect Phase 11 runtime
refactors.

## Implementation

Bumped with `node ./scripts/set-package-version.js 0.28.4` so the root
`package.json` and the legacy `packages/mux-compat` forwarding package
stay version-locked (the v0.28.3 bump missed the compat package and
broke `Test / Unit` on main, fixed in coder#4048).
`src/common/compat/productIdentity.test.ts` passes locally.

After this PR merges, the `v0.28.4` tag will be applied to the squash
commit and the GitHub Release published to trigger the
desktop/npm/docker pipelines.

---

_Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking:
`medium` • Cost: `$0.00`_

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=medium
costs=0.00 -->
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