Skip to content

fix(auth): let users choose which account to sign in with#2821

Open
AirZona wants to merge 3 commits into
block:mainfrom
AirZona:fix/oauth-account-selection
Open

fix(auth): let users choose which account to sign in with#2821
AirZona wants to merge 3 commits into
block:mainfrom
AirZona:fix/oauth-account-selection

Conversation

@AirZona

@AirZona AirZona commented Jul 25, 2026

Copy link
Copy Markdown

Fixes #2802

What problem this solves

Browser-handoff sign-in gives no way to choose which account you authenticate as. The login URL is opened in the default browser with no prompt parameter, so the identity provider silently reuses whatever session that browser already holds and returns straight to Buzz — the account chooser never appears.

Signing out doesn't rescue you, because sign-out is local-only: clear_builderlab_auth drops Buzz's in-memory session and cannot touch the browser's. So Sign out → Sign in is a no-op round trip that re-authenticates the same account. A user whose browser is signed into a personal account has no path to a work account, and vice versa.

How it was implemented

An explicit, user-initiated account-selection path that sends prompt=select_account:

Desktop (builderlab.rs)login_url() takes force_account_selection; start_builderlab_login accepts it as an optional command argument. Omitted (None) means today's behavior, so existing callers are unchanged.

Desktop UI (HostedCommunitiesSettingsCard.tsx) — a "Use a different account" action next to Sign out, and a second one alongside the signed-out Sign in button (the signed-out state is exactly where the silent-reuse trap bites, since the local session is already gone but the browser's is not). The signed-in path clears the local session before opening the browser, so a cancelled or failed switch doesn't leave a stale account rendered.

buzz-agent (auth.rs) — the authorize-URL construction is extracted from browser_pkce_flow into a pure authorize_url() function, which made it unit-testable, and the flag is threaded through. interactive_login() takes it, surfaced as buzz-agent auth <provider> --select-account.

Key decision: implicit auth is left alone

bearer()'s fall-through to the browser flow (step 5) passes false. That path is reached mid-request, not by a user who asked to switch accounts — forcing a chooser there is friction, and a provider that rejects prompt=select_account would start failing requests that work today. The chooser is only requested on flows the user explicitly initiates.

Tests

  • authorize_url_omits_prompt_by_default / authorize_url_requests_account_chooser_when_forced (crates/buzz-agent/src/auth.rs)

  • login_can_force_the_account_chooser, plus a !query.contains_key("prompt") assertion added to the existing login_defaults_to_auth0_login (desktop/src-tauri/src/builderlab.rs)

  • desktop/tests/e2e/hosted-communities-account-selection.spec.ts — end-to-end regression coverage: plain sign-in still sends forceAccountSelection: false, "Use a different account" sends true from both the signed-out and signed-in states, and the signed-in path clears the local session before opening the browser. It also captures the two card states (screenshots posted as a comment).

The unit-test pairs pin the default-off behavior as well as the new one, so a future change that starts forcing a chooser on implicit auth trips a test.

How to test it manually

  1. Sign into Buzz with a browser signed into account A.
  2. Click Use a different account → the provider's account chooser appears instead of bouncing straight through on A.
  3. Pick account B → Buzz shows B.

Follow-up deferred

The OAuth cache key in buzz-agent is sha256(discovery_url|client_id|scopes) (cache_path_for) — the authenticated account isn't part of it, so a machine still can't hold tokens for two accounts of the same provider. That's a cache-key migration and wants a maintainer opinion first; it's item 3 in #2802 and is not addressed here.

One thing I can't verify from outside: in the Builderlab path the IdP is Auth0 in front of the upstream connection, so prompt=select_account only reaches the upstream provider if Auth0 forwards it. This PR makes Desktop send it correctly; if the forwarding isn't configured, a server-side counterpart is needed. Flagging for someone with visibility into that config.

PR Checklist

  • cargo fmt --all --check, clippy clean on buzz-agent + Tauri (-D warnings)
  • Tests: cargo test -p buzz-agent, just desktop-tauri-test, just desktop-test (3489 passing), just test-unit, and the new Playwright spec under --project=smoke
  • just desktop-check (biome + file-size + px-text + pubkey-truncation guards)
  • Integration tests (just test) — not run; requires Postgres + Redis, and nothing here touches the relay, db, or auth crates
  • New public APIs are documented (doc comments on authorize_url, interactive_login, login_url, start_builderlab_login, startBuilderlabLogin)
  • No new unwrap() in production code paths
  • No new unsafe blocks

@AirZona
AirZona requested a review from a team as a code owner July 25, 2026 05:33
Browser-handoff auth opened the login URL with no `prompt` parameter, so
the identity provider silently reused whatever session the default browser
already held. A user whose browser is signed into a personal account had no
way to authenticate as a different (work) account, and signing out did not
help: `clear_builderlab_auth` drops only Buzz's own session, never the
browser's, so sign out -> sign in returned the same account without ever
showing a chooser.

Add an explicit account-selection path that sends `prompt=select_account`:

- desktop: `login_url` takes `force_account_selection`; `start_builderlab_login`
  accepts it as an optional command argument (defaults to today's behavior).
- desktop UI: "Use a different account" next to Sign out, and alongside the
  signed-out Sign in button. The signed-in path clears the local session
  first so a cancelled switch doesn't leave a stale account on screen.
- buzz-agent: extract the authorize-URL construction into `authorize_url`
  and thread the flag through `browser_pkce_flow`; `interactive_login` takes
  it, exposed as `buzz-agent auth <provider> --select-account`.

Implicit first-run auth is unchanged — no chooser is forced on a path the
user did not initiate, and providers that reject the parameter never see it.

Fixes block#2802

Signed-off-by: Tom Smallwood <tom@agilerocket.com>
@AirZona
AirZona force-pushed the fix/oauth-account-selection branch from 444ecae to 1432e2a Compare July 25, 2026 05:43
Regression coverage for the account chooser: asserts the plain sign-in
still sends `forceAccountSelection: false`, that "Use a different account"
sends `true` from both the signed-out and signed-in states, and that the
signed-in path clears the local session before opening the browser.

Also captures the two settings-card states for review, following the
existing *-screenshots.spec.ts pattern, and adds `hosted-communities` to
the `openSettings` helper's section union so the section is reachable from
tests like every other one.

Signed-off-by: Tom Smallwood <tom@agilerocket.com>
@AirZona

AirZona commented Jul 25, 2026

Copy link
Copy Markdown
Author

Screenshots

Both states of Settings → Hosted communities, captured with the repo's mock-bridge tooling.

Signed out — the new secondary action sits next to the primary sign-in, with a line explaining why you'd want it:

signed out

Signed in — "Use a different account" next to Sign out. This is the state the bug is about: the account shown is whichever one the browser session happened to hold, and before this change there was no way to get to another one.

signed in

Hosted from a commit-pinned raw.githubusercontent.com URL on my fork rather than via scripts/post-screenshots.sh, which pushes to a branch on block/buzz and needs write access I don't have as an outside contributor. Same immutable-SHA property, no relay-media URLs.

CI is waiting on approval

The CI and Docker image runs are sitting at action_required (first-time-contributor gate), so only DCO / Semgrep / zizmor have reported. Could a maintainer approve the workflow run? Everything that can be run locally is green:

  • just test-unit (full workspace), just desktop-test (3487), just desktop-tauri-test
  • just desktop-check, cargo fmt --all --check, clippy -D warnings on buzz-agent + Tauri
  • The new hosted-communities-account-selection.spec.ts (3 tests) passes under --project=smoke

This is safe to merge independently of any Builderlab-side change

Worth stating plainly, since I flagged the Auth0 question in the description and it shouldn't hold the PR up:

  • force_account_selection defaults to false, so every existing call site produces a byte-identical authorization URL. The change is a strict no-op unless the user clicks the new button.
  • If Auth0 doesn't forward prompt=select_account upstream, the new button degrades to exactly today's behavior — nothing regresses, and the Desktop half is already correct for whenever the forwarding is configured.
  • Implicit first-run auth in buzz-agent deliberately still passes false, so providers that reject the parameter never receive it.

Happy to split the crates/buzz-agent half into its own PR if you'd rather review the desktop and agent surfaces separately — just say the word.

…ection

Signed-off-by: Tom Smallwood <tom@agilerocket.com>

# Conflicts:
#	desktop/playwright.config.ts
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.

[Bug] No way to choose which account Buzz signs in with — browser-handoff auth silently reuses the existing browser session

1 participant