Repository navigation
fix: fix Web Console hanging or signing out when SSO tokens expire - #617
Conversation
Extracted from #603: the console half of questdb/questdb#7799 and questdb/questdb-enterprise#1267. Those PRs stop serving a request from the qdb_session cookie when its Authorization header is rejected, so an expired or stale bearer now gets a 401 and closes the session instead of being masked. - Refresh an ID-token bearer (acl.oidc.groups.encoded.in.token=true) by the ID token's exp claim rather than the access token's expires_in, and report a missing ID token instead of sending "Bearer undefined". - Read the bearer at send time for SQL, validation, CSV check/import and preference requests, and never let stored REST/Basic credentials replace an SSO bearer. - Coordinate token refresh in one bounded, cancellable place: queries wait up to 10s, a rotating refresh may still land within 60s, transient failures back off without logging out, and an expired bearer is never sent after a failed renewal (fixes the refresh deadlock that froze every query). - Recover from a rejected bearer: refresh and resend once (a 401 executed no SQL), ignore 401s answered for an older bearer, and log out only when the current bearer is rejected. - Show a renewal warning and a recovery view with Retry/Sign in again instead of a blank console, and report sign-in failures. - Share one client between the console and telemetry so telemetry never sends a stale bearer, and mark read-only requests as background so logout warnings only count statements and writes. - Dispatch the Enterprise Web Console e2e lane added by questdb-enterprise#1267, send the stored REST token in e2e execQuery, and extend the unit and stubbed browser auth tests.
Web Console deploy preview
|
…he session ends - Let uploads and settings saves that are already being sent finish when the session ends, instead of aborting them mid-stream: an aborted CSV upload can leave a partially imported table. Their results are still rejected as session-ended and no longer count as the next login's activity. Queries, and writes still waiting for renewal, are cancelled. - Show a failed SSO renewal in the Schema panel and reload it once a request succeeds, instead of leaving the tree blank. - Supersede notebook cell runs, auto-refresh rounds and headless agent runs before session end aborts their requests. Their aborted results no longer overwrite the last good snapshot, save refresh errors, delete chart snapshots or record a possibly committed write as a failed run. - Cover the session-end client behaviour, Schema recovery, notebook session end and run_query's unverified classification, and add an e2e test for renewing and resending an editor statement once after a 401.
Every 401 in the auth e2e tests was stubbed, so nothing checked the tandem server change (questdb/questdb#7799, questdb/questdb-enterprise#1267) against a real server. The new Enterprise OIDC test leaves /exec unstubbed. After a reload, silent re-auth installs a bearer the userinfo fixture rejects, while the browser still sends the qdb_session cookie of the earlier sign-in. The test checks that the server answers that session start with 401 instead of serving it from the cookie, and that the console renews its token once and resends its startup check once, then keeps working as the same user. Document this coverage, and that ID-token bearers still need an Enterprise lane with a fixture that signs ID tokens.
"Check if OSS is on master" failed on runners with the ubuntu-26.04 image, whose default Python is 3.14. pygit2 1.14.1 ships wheels only up to CPython 3.12, so uv built it from source and failed on the missing libgit2 headers (git2.h). Runs on ubuntu-24.04 still passed. Pin the interpreter so uv installs the prebuilt wheel on every image. The check itself is unchanged; the e2e/questdb pointer is the OSS master head.
Ending a session lets uploads and settings saves that are already being sent finish, because aborting a CSV upload mid-stream can leave a partially imported table. But "Sign in again" told users those uploads "may still finish", and the Login view's next step, signing in with SSO, navigates away and cancels them. - Keep counting sent writes after their session ends, until they settle. - Show that count on the Login view, and ask the browser to confirm leaving the page (SSO redirect, reload or closing the tab) until they finish. Logging out from the toolbar, which reloads at once, is unchanged. - Say in the "Sign in again" confirmation that uploads continue only while the tab stays open.
The only check of the tandem server change (questdb/questdb#7799) against a real server is in the Enterprise lane, which CI skips unless ENTERPRISE_E2E_ENABLED is set. Replay its startup sequence with stubs, for access-token and ID-token bearers: the server rejects the new bearer of both the session start and the startup check, and the console renews its token once, resends the check once and keeps the session.
A table check can now wait up to 10s for a token refresh. The Import view then replaced the whole file list with a copy taken before the wait. That reset a running upload to "Upload", so a second click could import the file twice. It also reverted edits and dropped files added meanwhile. - Merge only the checked status into the current list, and ignore a status checked for a name the file no longer has. - Apply renames and other edits at once, so an upload started during the check uses the new name. Only the status waits for the check.
After a 401, a query waited for the token refresh until its 60s network deadline. Queries waiting before their first request gave up after 10s, when the console already reported the renewal as failed, so a rejected statement kept running next to the failure warning. After a 401, wait no longer than before the first request. The refresh itself still runs to its network deadline, so a late rotated token is still installed for the next request.
Ending the session without a reload rejects the queries still running with "Your session has ended. Please sign in again." The editor and the AI chat stored that as the query's notification, so after signing in again with a password, the console still asked the user to sign in. - Editor, script and AI chat runs now say that the query was not run, or that it was interrupted and may have executed. This also replaces the "Running..." notification, whose Stop button would otherwise start a query. - A result page read interrupted by the session end adds no notification.
A failed SSO renewal is shown in the Schema panel. After the renewal recovered and the tables loaded, a later unrelated failure still showed "Could not renew your SSO session".
Identity providers often omit the full stop, e.g. Keycloak's "Token is not active", which read "Token is not active Please sign in again."
A 401 for a rejected bearer closes the server's cookie session, and the Enterprise server deletes the qdb_session cookie with every 401. The console renewed its token and resent the query, but never asked for a new session. Downloads authenticate only with that cookie, so CSV and Parquet exports failed with "Unauthorized" until the page was reloaded. After a 401, the next SQL request of the same SSO login now sends session=true, one request at a time, until the server accepts one. The server opens a session for it only when the browser sent no cookie, so the parameter changes nothing while the cookie is still valid.
Renaming the target table of a queued CSV applied the new name at once, but kept the old name's status until the existence check answered. The check can wait up to 10s for a token renewal, and a failed check never answered. A file renamed to an existing table could show "Ready to upload", and its upload appended to that table without the "Table already exists" warning. The new name now shows "Checking table name…" and can't be uploaded until its check answers. A failed check shows "Could not check table name" instead of the old status.
Ending a session lets uploads and settings saves that are already being sent finish, and Login asked before leaving the page until they did. But signing in again with a password replaces Login without leaving the page, so the notice and the leave-page prompt disappeared while the upload was still being sent. A reload then cancelled it without asking, which can leave a partially imported table, and the empty Import view invited importing the same file again. - Ask before leaving the page in every view, not only on Login, until the previous session's writes settle. - After signing in again, show a warning with their live count in the console until they settle. - Logging out from the toolbar still reloads at once. Asking there could keep a console without a session, as Login is not shown before the reload. The warning says that logging out cancels them.
Ending the session aborts a running script's statement and stops the script before its next one, but nothing tested it, and the end of the run failed: the editor is unmounted while the script settles, and revealing the position of an editor without a model threw "Invalid arguments". The run was never settled, so the editor stayed marked as running a script and the query log missed its "Stopped after..." summary. - Settle the run when the editor no longer has a model. - Check for a stop before the first statement too, so a session that ends while the script waits to start sends nothing. - Test both ways a session ends during a script, with stop after failure off: signing in again while a statement runs, and a 401 that signs the user out. The script stops before its next statement, and the log says whether the interrupted statement may have executed or was not run.
The three .gitleaksignore entries added here fingerprint lines 102-104 of e2e/tests/enterprise/oidc.spec.js in commit 0f4d4ac. That commit is on the #603 branch, not in this branch's history, and those lines hold no token here. Every token this branch adds to oidc.spec.js is marked with gitleaks:allow, so the entries suppressed nothing. #603 carries its own copy of them.
The server closes the session of a request whose bearer it rejects, and Enterprise deletes the qdb_session cookie with every 401. The console asked for a new session (session=true) only after an /exec 401. Live validation, CSV checks, uploads and settings saves send the same cookie, and validation runs while the user types. If one of them saw the 401, and the bearer was renewed before any query was rejected, no query asked for a session. CSV and Parquet exports, which authenticate with the cookie alone, then kept failing. Record the lost session whichever request saw the 401. While the cookie is still valid, asking for a session changes nothing.
A table check can wait for a token renewal and then fail, e.g. after a failed SSO renewal. Only a renamed file handled that. A dropped file was silently lost: it never appeared, and no error was shown. And one failed recheck, when the Import view became visible, discarded every other file's new status. - List a dropped file whose check fails as "Could not check table name", like a renamed one, and let it be uploaded. - Keep a file for an existing table whose schema can't be read, without a pre-filled schema. QuestDB then infers it from the file. - Keep a file's status when its recheck fails, and merge the others. - Count dropped files as queued at once, so that dropping one again while its check waits is reported as a duplicate, not queued twice.
After a failed SSO renewal the user stays signed in, but statements are not executed. Auto-refresh rounds treated that like a failed query. A chart's frame was replaced with errors and its saved snapshot deleted, and a grid saved "Could not renew your SSO session" as a refresh error, shown again after the next sign-in. Rounds cut off by the end of the session are already dropped. Treat a statement that was not executed like an aborted one. A chart keeps its frame and its saved snapshot. One with nothing to show yet shows the failure, but still keeps its snapshot. A grid slot keeps its rows, and the round saves nothing.
A failed SSO renewal was marked in the editor like a SQL error: a red marker on the query's first word, with the cursor moved there. A session end already only replaced the "Running..." notification. The messages also said "not sent" of requests that the server had received and rejected with 401 or 403. What holds in every case, and tells the user that a retry is safe, is that nothing was executed. - Show a failed renewal in the notification only, for single runs and scripts, without marking the query. - "Not executed: your previous session ended without running this query." replaces "Not run: your previous session ended before this query was sent." - A failed renewal says "the request was not executed" instead of "the request was not sent", and the recovery view no longer says "No request was sent."
The store outlives the console: signing in again with a password remounts it in the same document, without a reload. The result grid then showed the previous login's last rows, possibly to another user, and scrolling re-ran its query with the new credentials. Clear the result when the session ends. Notifications stay: they tell whoever signs in next what the end of the session interrupted. Also correct the logout comment, which described a store reset that did not happen.
Review of PR #617, level 3 (full review)Reviewed at Quality gate: everything passes.
PR title and description
Issues
False positives
Summary
|
Verdict clarificationThe previous comment's verdict ("request changes (minor)") was unclear. It should be request changes. The items to fix are listed below by their #ID and title in the Issues table. Blocking: regressions against
|
After a failed SSO renewal, the recovery view's Retry did nothing for 5 to 30 seconds: the backoff between failed renewals refused it at once, without a request, and the view looked unchanged. While a renewal that had timed out was still waiting for the identity provider, for up to 50 seconds, Retry was refused the same way. The view still said "Retry to renew it". - Retry skips the backoff between failed renewals, which protects the identity provider from background requests. The throttle after a successful renewal still applies. - If a renewal is still in flight, Retry waits for its answer instead of starting another, which could spend a refresh token the first has rotated. It shows "Reconnecting…" meanwhile. - The recovery view no longer asks to wait before retrying. The renewal warning says that queries retry the renewal after a short wait. They keep the backoff, so that a script doesn't renew once per statement.
A refresh round with a statement that was not executed, e.g. after a failed SSO renewal, kept any frame the cell held, and didn't settle. After an edit, that frame was for the old SQL, which the chart doesn't draw. The cell showed "Drawing…" with no error until a later round reached the server. Keep the frame only if the chart draws it for the current statements, using the same test as DrawCanvas. Otherwise show the failure, as for a chart with no frame, and still keep the saved snapshot.
A failed settings refresh unmounts the console, and Retry mounts it again without ending the tab's login. If another tab had logged out meanwhile, removing the shared password credentials, Login was shown while this tab still counted as signed in. The next sign-in then kept that session: requests of the previous login could still complete into it, and none of the session-end cleanup ran, so its last result stayed. End the session before showing Login in that case, and explain the interrupted requests as logout does. Stored credentials and the server session are left alone.
- An agent's run on a closed notebook was dropped whole when the session ended, so the cell kept its previous run on record, even for writes that had already committed, and they could be repeated. Only a run that has not launched is cancelled now. A launched run records the statements that completed, and the one in flight as interrupted, with its outcome unverified. Its remaining statements are not sent without the login's credentials, and are recorded as not executed. A saved result says what the end of the session did to the query, instead of asking to sign in again. - A chart whose frame for its SQL had no rows to draw, e.g. the error of a run kept when the cell became a chart, still showed "Drawing…" forever after a round that was not executed. Keep only a frame that the chart draws. - A CSV file dropped again while the table check of its first drop waited for a token refresh could still be queued twice: any change to the list, and showing the list for the first time, forgot the pending name. ImportCSVFiles now tracks the dropped files that are not listed yet, and the drop box counts them as queued. - "Sign in again" removed the SSO session flag that other tabs share, so another SSO tab that reloaded showed Login instead of signing in silently. It now ends only this tab's session. - When the session ended, the Schema panel took its interrupted columns load for an unsupported function, and sent the fallback query without credentials. It now stops. After a failed renewal it doesn't try the fallback either, as the server ran no query. Answers that arrive after the session ended are not stored. - The tables and columns of the previous login stayed in the store after a session end without reload, and fed autocomplete, the AI assistant and CSV import for whoever signed in next. They are now cleared with the last result.
Review of PR #617, level 3 (full review), round 2Reviewed at
Structural, test-coverage and unchanged prior findings were checked directly against the source. Quality gate: everything passes locally.
CI at
PR title and description
Status of the previous review's findings (at
Issues
False positives
Summary
|
- Ending the session of an SSO tab, by logging out, signing in again or after a 401, removed the password credentials that tabs share, so another tab signed in with a password showed Login once it reloaded. Only a tab signed in with a password removes them now. An SSO tab also keeps its login method, so that reloading it resumes SSO or shows Login, instead of signing in with another tab's password. - Logging out from the toolbar reloads the page at once, which cancelled running statements and uploads being sent without asking, although a cancelled CSV upload can leave a partially imported table. It now asks first, and keeps the session if declined. - Logging out from the toolbar also cleared the last result, tables and columns before the reload, blanking the console for a moment. The reload discards them anyway, so they are left to it. - When the session ended, an agent's read-only run on a closed notebook recorded its reads as interrupted or not executed, replacing the previous run and its saved rows. A read-only run is now cancelled like one that has not launched, so the previous run stays on record. A run that writes still records what it did. - Once the token had expired, a statement run again while a timed-out renewal still waited for the identity provider, for up to 50 seconds, failed at once without a new renewal, although the renewal warning says that running it again after a short wait retries the renewal. After the backoff, a statement the user runs now waits for that renewal to be answered or given up, and then renews. Background reads, and statements run during the backoff, still fail at once. - The one-off telemetry config read at startup never renewed the session, so it failed near expiry or after a failed renewal, e.g. at every login with a token that lives under 30 seconds. Metrics then asked to enable telemetry, and console events were not tracked, until the next login. The read may now renew the session, like the console's other reads. - If the request that opens the cookie session at SSO sign-in was rejected or could not be sent, downloads could fail for the whole login: only a later 401 made a request ask for a new session. The next SQL request now asks for one in that case, and a network error there no longer escapes as an unhandled rejection. - A chart without a frame it could draw deleted its saved snapshot on the second round in a row that was not executed: that round matched the failure the first one showed, and an unchanged frame without rows cleared the snapshot. The snapshot now stays until a round reaches the server.
Review of PR #617, level 3 (full review), round 3
Reviewed at
Style, structure and test-gap findings were checked directly against the source. Round-2 items were re-checked at HEAD by the domain agents. Quality gate: everything passes locally.
CI at
PR title and description
Status of round-2 findings
The core client, refresh and session-fence machinery held up again under every path traced:
IssuesNew in this round
Still present from round 2All of these were re-checked at
False positivesThese claims, or parts of claims, did not survive verification:
Summary
|
Tandem: questdb/questdb#7799, questdb/questdb-enterprise#1267
Extracted from #603. This PR contains only the console changes the two server PRs depend on; everything else stays in #603.
Why the server change needs it
questdb/questdb#7799 and questdb/questdb-enterprise#1267 tie HTTP sessions to the credentials that opened them. A request that carries the
qdb_sessioncookie and anAuthorizationheader the server rejects is no longer served from the cookie: it gets 401, and the session is closed. Until now, that cookie fallback hid stale bearers from the console. From now on, the console has to send a valid bearer and recover from a 401 itself.Changes
acl.oidc.groups.encoded.in.token=truethe bearer is the ID token. The console now schedules its renewal from the ID token'sexpclaim, instead of the access token'sexpires_in. A provider that omits the ID token gets an explicit "Missing ID token" error instead ofBearer undefined.qdb_sessioncookie. Downloads authenticate with that cookie alone, so the next SQL request asks the server for a new session (session=true). It does the same if the request that opens the session at SSO sign-in is rejected or can't be sent..github/workflows/enterprise_e2e.ymldispatches the Azure lane that questdb-enterprise#1267 adds.feat/qwp-entra-token-providerfor that reason, so it runs against the tandem server.oidc.spec.jschecks the 401 recovery against a real server, with/execunstubbed, including that a cookie-only export works afterwards. The required auth lane replays that startup sequence with stubs, for both bearer kinds.e2e/commands.jsexecQuerynow sends the stored REST token for Enterprise password logins.e2e/questdbis bumped to OSSmaster(f21c6570). fix: fix Web Console hanging when SSO token refresh fails #603 pinse4da5b45, 4 commits behind. "Check if OSS is on master" requires every PR's pointer to be within 5 commits of OSSmaster, andmain's pointer is 31 commits behind. The check now runs on Python 3.12, because pygit2 1.14.1 has no wheels for the newer runner image's Python.Left in #603
Kept out of this PR because they aren't needed for the server change:
The hybrid files carry only the part needed here; #603 keeps the rest.
Toast/index.tsx: onlyREFRESH_FAILED_TOAST_ID.refreshFailedWarning.tsx: no Alt+N instructions.index.tsx: the ToastContainer move, andclearLoginDataOnSessionEnd(store.dispatch), which clears the last result, tables and columns when the session ends.backgroundread flag.backgroundread flag and thessoAuthStaterename, plus the session-end, renewal and table-check handling described above.auth.spec.js: only the tandem tests. The "Sign in again" focus test clicks the warning directly instead of using Alt+N.Once this is merged, merge
maininto #603. I rehearsed this at58add5e2: it had 6 conflicts (auth.spec.js,Toast/index.tsx,refreshFailedWarning(.test).tsx,AuthProvider.test.tsx,Schema/index.tsx), and resolving them with #603's version gave #603's tree. #603 has none of the commits added here since (05335a00to93d55dd2), and with #603 at76401b41the merge now conflicts in 41 files. Resolving those with #603's version would drop these fixes, and merging #603 would then revert them onmain. Keep them when resolving, or port them to #603 first. Two of the conflicts need a choice rather than a side:e2e/questdb: keepf21c6570or newer. fix: fix Web Console hanging when SSO token refresh fails #603'se4da5b45falls out of the 5-commit window as OSSmastermoves.src/index.tsx: only the imports conflict. Both session-end resets merge cleanly below them: fix: fix Web Console hanging when SSO token refresh fails #603'sauthSessionState.onEndlistener, which dispatchesqueryActions.resetSession()and so returns the whole query state to its initial value, notifications included, and this PR'sclearLoginDataOnSessionEnd(store.dispatch), which keeps the notifications. Keep exactly one. Taking fix: fix Web Console hanging when SSO token refresh fails #603's imports leaves this PR's call without its import, and keeping both runs both resets.Merge order
web.console.versionto that releaseValidation
At
93d55dd2:yarn typecheck,yarn lintandyarn buildpass.yarn test:unitpasses: 2,996 tests in 149 files.yarn cypress run --spec 'e2e/tests/auth/*.spec.js' --browser electronagainst the production preview: 48 passing (44 auth, 4 unauthorized-recovery), none retried.Not run locally:
ENTERPRISE_E2E_ENABLED(CI skips it until that is set) and the pipeline from questdb-enterprise#1267, including its checks that the resend re-opens the cookie session. As configured there, it usesgroups.encoded.in.token=false, so the ID-token path is covered only by unit tests and the stubbed browser tests.