fix: skip the empty account picker during an active login ceremony - #100
Closed
yahyafakhroji wants to merge 5 commits into
Closed
fix: skip the empty account picker during an active login ceremony#100yahyafakhroji wants to merge 5 commits into
yahyafakhroji wants to merge 5 commits into
Conversation
prompt=select_account returned /accounts without consulting hasSessions, so a first-time datumctl login landed on an empty account picker. Every other decideAuthorize branch already considers session state; the SAML entry path was hardened the same way in authorize.service.ts:391-396. Adds the first tests for this branch — it had none, despite three source comments warning about select_account loops. Refs #99
switchSchema and removeSchema each carried an inline copy of the same max(64) + charset bound. The /accounts loader is about to become a third consumer, so hoist it to one source with boundary tests. No behavior change. Refs #99
The loader is about to need the exact target the component links to. Sharing one function keeps the link and the redirect from drifting. No behavior change. Refs #99
An empty account picker holding a requestId is a dead end — nothing to select, and the only control is "Add an account". Redirect straight to the identifier screen, carrying the ceremony so login resumes the OIDC/SAML/device callback. Scoped to ceremony-present: a bare empty /accounts still renders the empty state, which is the correct answer for a logout probe, a bookmark, or the legacy /ui/v2/login/accounts 301. Also validates user_code in the loader — it now feeds an automatic 302 rather than a link a human clicks. Closes #99
…est form The loader's ceremony test read only ?requestId= and ?user_code=, but a ceremony legitimately arrives in Zitadel's raw shape too — normalizeRequestId folds ?authRequest= into oidc_<id> and ?samlRequest= into saml_<id>. The legacy /ui/v2/login/accounts 301 preserves the query verbatim and 'accounts' is in KNOWN_SEGMENTS, so /ui/v2/login/accounts?authRequest=abc reached the picker with an undetected ceremony: it rendered the empty state and "Add an account" resolved to a bare /login, dropping the ceremony. Reuses normalizeRequestId rather than re-deriving the mapping, so the raw shapes stay in sync with /authorize. A threaded requestId still wins. Refs #99
Contributor
🧪 Test Summary
|
yahyafakhroji
enabled auto-merge
July 20, 2026 14:46
auto-merge was automatically disabled
July 20, 2026 14:52
Pull request was closed
yahyafakhroji
deleted the
yahyafakhroji/improve-datumctl-login-experience-on-first-time
branch
July 20, 2026 14:52
Collaborator
Author
|
Superseded by #101 (branch renamed to |
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.
Closes #99
Problem
A first-time
datumctl loginopened the browser on "Choose an account" with an empty list — "No signed-in accounts" and a single "Add an account" link. Users had to click a picker with nothing in it before reaching the login form.datumctlsendsprompt=select_accountunconditionally, anddecideAuthorizehonored it without consulting session state — the only branch that didn't. The SAML entry path was hardened against this exact failure mode already (authorize.service.ts:391-396); OIDC was never brought into line.Fixed in auth-ui rather than the CLI:
staff-portalanddatum.netalso sendselect_account, so a CLI-side fix would leave them exposed to the same dead end.Approach
The deciding question is not "is the list empty" but "is there a ceremony to resume." An empty picker holding a
requestIdis a dead end. An empty picker without one is a valid answer to a valid question — "am I signed out?"decideAuthorizenow routes a session-lessselect_accountto/login. The/accountsloader is the backstop for the eight entry points that bypass it —/accountsis unguarded and reachable by direct navigation, bookmark, and the legacy/ui/v2/login/accounts301.?requestId=oidc_…/login?requestId=oidc_…(+organization)?authRequest=…/?samlRequest=…/login?requestId=oidc_…/saml_…?user_code=WXYZ/login?requestId=device_WXYZ?user_code=<malformed>?requestId=evil_payload/accountsScoping to ceremony-present rather than any-empty-list keeps the bare empty state intact.
acceptance/logout.acceptance.cy.tsvisits bare/accountsafter logout and asserts it stays put — a security assertion proving a logged-out session isn't silently resurrected. It passes unmodified, as do the four pre-existing loader tests.Note
hasSessionscounts raw cookie entries including expired ones, while the loader filters vialistAccounts. The loader redirect is therefore load-bearing on the primary OIDC path too, for any user with a stale cookie — thedecideAuthorizechange saves a round trip and restores OIDC/SAML symmetry.Post-login resume is unchanged and already worked: a fresh login carrying a
requestIdgoes straight to/authorize→runCallback→ the RP callback (login.service.ts:478-487).Supporting changes
userCodewas bounded inswitchSchema/removeSchemabut read raw in the loader. Fine when it fed a link a human clicks; it now feeds an automatic 302, so the bound is shared across all three consumers.addAccountHrefis now one exported function, so the component's link and the loader's redirect can't drift apart.normalizeRequestId, so a ceremony arriving in Zitadel's raw form is recognized. Previously/ui/v2/login/accounts?authRequest=abcreached the picker with an undetected ceremony and "Add an account" dropped it.Testing
540 component tests pass;
lint:ci,typecheck,typecheck:cypress,lint:cycles,lint:boundariesclean.New coverage:
decideAuthorize'sselect_accountbranch (previously untested entirely, despite three source comments warning aboutselect_accountloops); loader redirects foroidc_/raw/device_ceremonies withorganizationpreserved; malformeduser_coderejection; bare-URL 200 as a regression guard;userCodeSchemaboundaries.