fix(auth): let users choose which account to sign in with#2821
Conversation
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>
444ecae to
1432e2a
Compare
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>
ScreenshotsBoth 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 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. Hosted from a commit-pinned CI is waiting on approvalThe
This is safe to merge independently of any Builderlab-side changeWorth stating plainly, since I flagged the Auth0 question in the description and it shouldn't hold the PR up:
Happy to split the |
…ection Signed-off-by: Tom Smallwood <tom@agilerocket.com> # Conflicts: # desktop/playwright.config.ts


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
promptparameter, 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_authdrops 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()takesforce_account_selection;start_builderlab_loginaccepts 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 frombrowser_pkce_flowinto a pureauthorize_url()function, which made it unit-testable, and the flag is threaded through.interactive_login()takes it, surfaced asbuzz-agent auth <provider> --select-account.Key decision: implicit auth is left alone
bearer()'s fall-through to the browser flow (step 5) passesfalse. 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 rejectsprompt=select_accountwould 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 existinglogin_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 sendsforceAccountSelection: false, "Use a different account" sendstruefrom 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
Follow-up deferred
The OAuth cache key in
buzz-agentissha256(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_accountonly 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 onbuzz-agent+ Tauri (-D warnings)cargo test -p buzz-agent,just desktop-tauri-test,just desktop-test(3489 passing),just test-unit, and the new Playwright spec under--project=smokejust desktop-check(biome + file-size + px-text + pubkey-truncation guards)just test) — not run; requires Postgres + Redis, and nothing here touches the relay, db, or auth cratesauthorize_url,interactive_login,login_url,start_builderlab_login,startBuilderlabLogin)unwrap()in production code pathsunsafeblocks