Skip to content

Preserve the document session across ClickHouse auth loss and reauthentication #512

Description

@BorisTyshkevich

Architecture decision

Temporary loss of ClickHouse authentication must suspend the authenticated
execution session without terminating the user's in-memory document session.
Tabs, drafts, raw Spec text, dirty state, workspace projection, navigation,
and completed results belong to the long-lived in-RAM document session. Queries,
exports, graphs, Dashboard refreshes, catalog requests, credentials, abort
controllers, and stale-completion fencing belong to a disposable authenticated
execution scope.

Completed results survive an in-place Phase 2 suspension only. The Phase 3
OAuth redirect checkpoint contains recoverable authored tab state, never
results or other transient execution state.

The connection lifecycle must be explicit rather than inferred from
serverVersion, authConfirmed, token presence, or one-shot callback latches.
The header connection chip is the authoritative user-facing projection of that
lifecycle. Explicit Log out remains a terminal end-of-session action;
involuntary auth loss does not.

This issue absorbs #502, #520, and #522 and supersedes the local latch/teardown
approach in PR #521. Its tests and review findings remain required evidence.

Branch model: one PR per phase off fresh main. Each phase must be green and
self-consistent on main.

Phases

Delivery phases

Phase 1 — Connection lifecycle foundation and authoritative status

Implement:

  • Add an explicit connection lifecycle with distinct starting, connected,
    refreshing, offline, auth-required, reauthenticating, and explicitly
    signed-out states.
  • Give each authenticated connection an epoch. A completion captured under an
    older epoch must never publish tokens or connection state into a newer one.
  • Make OAuth refresh single-flight per epoch so concurrent callers share one
    refresh attempt.
  • Replace the header chip's serverVersion-derived boolean with the
    authoritative lifecycle projection. Use green for connected, neutral/amber
    for connecting/refreshing/offline, and red plus text for auth-required.
  • Preserve the current auth-loss rendering behavior in this foundation phase;
    Phase 2 changes suspension UX after the state and concurrency rules exist.

Acceptance gate:

Concurrent refresh and late-settlement tests must prove that an old epoch
cannot resurrect credentials or overwrite the current connection state.

Phase 2 — Authenticated execution scope and in-shell suspension/resume

Implement:

  • Separate the long-lived document session from a disposable authenticated
    execution scope.
  • The execution scope owns or registers Workbench, Dashboard, export, graph,
    catalog, documentation, and detached-result operations and their cancellation
    or stale-settlement fences.
  • On involuntary auth loss, close the scope exactly once, abort local work,
    prevent late writes to History/results/schema/tokens, invalidate
    connection-scoped caches, and retain the document session and completed
    results in RAM.
  • Server-side cancellation is best-effort through an immutable cancellation
    lease captured by the closing scope: exact target origin plus a prebuilt
    Authorization header. It must have no refresh/retry path, call back through
    the failing live authentication path, or lazily re-read a cleared auth mode.
  • Keep the application shell mounted. Show red Sign in required connection
    status and reuse the existing authentication controls in-shell. Local editing
    and the dirty-tab unload guard remain active; ClickHouse-dependent commands
    are disabled or routed to the shared auth-required action.
  • Successful in-place Basic authentication (or a future non-navigating auth
    flow) creates a fresh execution scope, reloads connection-scoped metadata,
    and resumes the same document session. OAuth reauthentication resumes through
    Phase 3's fresh bootstrap and validated checkpoint restoration.
  • Explicit Log out remains destructive: close the scope, clear credentials
    and recovery data, and render the ordinary signed-out login experience.
  • Cover and close onAuthLost bypasses mandatory authenticated-session teardown #502 and Basic-mode teardown requests (KILL QUERY/export-cancel) get the wrong Authorization scheme after credentials clear #520 when this phase lands; retain and re-verify
    Detached-view (Data Pane Expand) refresh failure never reports auth loss #522's already-landed evidence under the shared execution-scope boundary.
    Close PR fix(#502): run mandatory authenticated-session teardown on involuntary auth loss too #521 as superseded only after the replacement is merged.

Acceptance gate:

One auth-loss transition must quiesce every authenticated operation owner,
preserve the exact document-session objects in RAM, and make all old-scope
completions observationally inert.

Phase 3 — OAuth redirect checkpoint and document-session restoration

Implement:

  • Add the versioned, workspace- and OAuth-state-bound recovery snapshot and
    pre-redirect transaction described below.
  • Snapshot the complete recoverable authored tab state only when save-relevant
    dirty work exists; retain an existing valid payload across failed
    callback/retry and replace only its OAuth attempt state.
  • Arm a one-shot beforeunload bypass only after the snapshot write succeeds,
    then navigate.
  • After a successful callback, load the committed workspace normally, restore
    and revalidate the document session before first signed-in render, reconcile
    linked saved-query tabs, and consume the snapshot only after success.
  • Keep the legacy oauth_shared path deterministic: a valid recovery payload
    wins and suppresses (never merges with) shared-query tab seeding; invalid or
    absent recovery follows the normal legacy shared behavior.
  • Add real-browser coverage for expiry → auth-required → OAuth redirect →
    restored dirty document session.

Acceptance gate:

A failed snapshot transaction must leave the page, dirty guard, and in-shell
auth-required UX usable; a successful round trip must restore authored state
without restoring transient execution state.

Tests by phase

Phase 1

  • Pure lifecycle transition and illegal/stale-event tests.
  • Single-flight refresh success/failure/concurrency tests.
  • Epoch tests proving a late refresh cannot restore a superseded session.
  • Header chip DOM, color-class, title, mobile text, ARIA, and state-transition
    tests.
  • Architecture test preventing connection readiness from being inferred from
    serverVersion outside the catalog/version display.

Phase 2

  • Port PR fix(#502): run mandatory authenticated-session teardown on involuntary auth loss too #521's deferred Workbench teardown test to execution-scope closure.
  • Cover Workbench, Dashboard, graph, both export modes, catalog/documentation,
    and detached-result refresh under one auth-loss wave.
  • Prove Basic and OAuth server-cancel requests use an immutable pre-clear
    Authorization header and never recurse into auth loss.
  • Prove repeated/concurrent loss signals close one scope once.
  • Prove local editor identity, drafts, dirty state, selection/route, completed
    results, and unload guard survive suspension.
  • Prove old-scope completions cannot record History, successful-result columns,
    bound parameters, schema, tokens, or UI settlement.
  • Preserve the rule that a post-confirmation query-level 401/403 is not
    automatically classified as auth loss.
  • E2E the red auth-required chip, in-shell authentication controls, disabled
    server actions, local editing, and resume after in-place Basic authentication.

Phase 3

  • Snapshot encode/decode, invalid Spec text, tab-id allocation, storage failure,
    workspace mismatch, expiry, callback mismatch, retry state replacement,
    restore reconciliation, and one-shot unload suppression.
  • A post-PKCE snapshot failure rolls back verifier/state/return-route for that
    attempt, or the next attempt atomically replaces all of them before redirect.
  • OAuth callback/bootstrap ordering and deterministic oauth_shared
    precedence.
  • E2E the dirty-tab expiry flow and successful OAuth restoration without an
    unload warning.

Problem

An expired OAuth session can lose or strand an unsaved tab during the existing re-login flow.

Reproduction

  1. Work normally with a dirty query or Dashboard-variable tab.
  2. Start an operation that requires ClickHouse access after the OAuth session has expired.
  3. The existing authentication UI is shown.
  4. Click the existing OAuth sign-in action.
  5. The browser warns that unsaved changes may be lost because the dirty-tab beforeunload guard is still active.
  6. Cancelling that navigation leaves the user on the authentication UI with no useful path back to the unsaved tab.
  7. Continuing the redirect reloads the application and loses the in-memory draft.

Scope

Do not add another authentication-required screen, modal, or Continue editing action. Reuse the existing expired-session/login UI and OAuth action.

The missing behaviour is preservation of the current tab session across the existing OAuth redirect.

Intended behaviour

1. Prepare the OAuth redirect

The OAuth flow must expose a pre-redirect step after PKCE verifier/state generation but before assigning location.href.

For an app-initiated OAuth redirect:

  1. Generate the OAuth state and verifier.
  2. Build and write a versioned tab-recovery snapshot to sessionStorage.
  3. Scope the snapshot to:
    • the generated OAuth state;
    • current workspace id;
    • current workspace key.
  4. Only after the snapshot write succeeds, arm a one-shot bypass of the dirty-tab beforeunload guard.
  5. Assign location.href and begin the OAuth redirect.

If snapshot serialisation or sessionStorage.setItem() fails:

  • do not redirect;
  • do not suppress beforeunload;
  • keep the existing authentication UI usable;
  • report that sign-in could not start without risking unsaved work.

Any OAuth verifier/state written before that failure must be rolled back or safely replaced by the next attempt.

2. Recovery snapshot

Use a dedicated, versioned recovery shape. Do not serialise QueryTab wholesale.

The snapshot must include:

  • version;
  • createdAt;
  • current workspaceId and workspaceKey;
  • current OAuth attempt state;
  • tab order;
  • active tab id;
  • nextTabId or an equivalent value that guarantees future tab ids cannot collide;
  • for each tab:
    • id;
    • doc kind/binding, including dashboard-variable bindings;
    • name;
    • sqlDraft;
    • raw specText;
    • specVersion;
    • editorMode;
    • dirtySql and dirtySpec;
    • savedId;
    • lastCommittedQueryToken;
    • externalState when present.

Do not restore transient execution state, including:

  • query results;
  • ClickHouse chSession ids;
  • last-result column metadata;
  • running/exporting state;
  • parsed Spec or diagnostics.

Re-evaluate the restored raw specText through the normal Spec parser/validator so invalid in-progress JSON drafts survive exactly as authored and diagnostics are rebuilt normally.

3. OAuth retry and callback handling

A failed OAuth callback or IdP error must not discard the recovery payload.

When the user retries sign-in:

  • retain the existing tab payload;
  • generate a new OAuth state/verifier;
  • replace the snapshot's attempt state with the new state before redirecting.

A stale callback whose state does not match the snapshot's current attempt state must never consume or restore the snapshot.

4. Restore after successful OAuth

After a valid OAuth callback:

  1. Complete token exchange.
  2. Restore the saved route through the existing OAuth return-route path.
  3. Load the current committed workspace through the normal boot path.
  4. Before the first signed-in surface render, validate and restore the tab snapshot.
  5. Reconcile linked saved-query tabs against the loaded committed workspace using the existing linked-tab conflict/deletion behaviour.
  6. Recompute Spec parsing and diagnostics from each restored specText.
  7. Restore tab order, active tab, editor mode, dirty flags, and safe tab-id allocation.
  8. Reinstall the ordinary dirty-tab beforeunload guard from the restored dirty state.
  9. Remove the recovery snapshot only after restoration succeeds.

A recovered draft must never be auto-saved or written into the committed workspace during restoration.

5. Workspace and snapshot validation

Do not apply a snapshot when:

  • its version is unsupported;
  • it is malformed;
  • it is expired;
  • its OAuth attempt state does not match the valid callback state;
  • its workspace id or key does not match the workspace loaded after callback.

Use a bounded expiry time so abandoned OAuth attempts do not retain SQL indefinitely in sessionStorage.

On workspace mismatch or malformed/expired data:

  • do not modify the loaded workspace or current tab session;
  • clear the unusable recovery snapshot;
  • continue through the normal signed-in boot flow.

6. beforeunload bypass

The unload bypass must be one-shot and apply only to the intentional OAuth redirect whose recovery snapshot was written successfully.

It must not disable or weaken the normal warning for:

  • reload;
  • tab/window close;
  • unrelated navigation;
  • OAuth redirect attempts whose snapshot write failed.

7. Dashboard-variable tabs

Preserve dashboard-variable tab bindings and dirty drafts across the OAuth round trip.

This issue does not add new optimistic-concurrency or conflict detection for Dashboard-variable option SQL. After restoration, those tabs keep the current Save semantics. Adding a baseline token/conflict flow for variable configurations is separate work.

8. Other lifecycle rules

  • Explicit user-selected Log out remains a separate end-of-session policy and must clear any pending OAuth tab-recovery snapshot.
  • A valid dirty-tab recovery snapshot takes precedence over the legacy single shared-query oauth_shared restoration path. Do not merge both into the same initial tab session.
  • Clean sessions should not create unnecessary recovery payloads; normal route restoration must remain unchanged.

Safety rules

  • Never silently discard or auto-save a dirty draft because authentication expired.
  • Never restore a snapshot into a different workspace.
  • Never overwrite committed workspace content while restoring tabs.
  • Never suppress beforeunload unless a valid recovery snapshot has been written for that exact OAuth attempt.
  • Never restore transient ClickHouse execution/session state.
  • Keep using the existing authentication UI and OAuth action.

Acceptance criteria

  • Connection readiness is an explicit, authoritative lifecycle with
    single-flight refresh and epoch-fenced settlement; it is not inferred from a
    server-version probe, token presence, or a callback latch.
  • The header chip distinguishes connected, connecting/refreshing, offline,
    auth-required, and explicitly signed-out states using text/ARIA as well as
    color; auth-required is red and says sign-in is required.
  • Involuntary auth loss closes one authenticated execution scope exactly
    once while preserving the in-memory document session, local editing, dirty
    guard, current workspace/route, and completed results.
  • Workbench, Dashboard, graph, export, catalog/documentation, and detached
    result operations are aborted or fenced so old-scope completions cannot
    mutate tokens, History, results, parameters, schema, or current UI.
  • Best-effort server cancellation uses an immutable authorization snapshot
    captured before credentials/auth mode change and never re-enters the failing
    live authentication path.
  • Reauthentication creates a fresh execution scope and resumes the same
    document session; explicit Log out remains the destructive end-session path.
  • Auth expiry continues to show the existing authentication UI; no additional modal or screen is introduced.
  • OAuth exposes a pre-redirect hook/step after state generation and before navigation.
  • Starting OAuth with dirty tabs writes a versioned, workspace-scoped recovery snapshot before redirecting.
  • A failed snapshot write prevents navigation and leaves the normal unload guard active.
  • After a successful snapshot write, the intentional OAuth redirect does not show the browser unload warning.
  • Ordinary reload, close, and unrelated navigation still show the dirty-tab warning.
  • A successful OAuth round trip restores tab order, active tab, SQL/Spec drafts, dirty flags, document bindings, editor mode, and non-colliding future tab ids.
  • Invalid raw Spec text survives the round trip and is revalidated after restoration.
  • Results, running state, chSession, and other transient execution state are not restored.
  • Dirty drafts remain dirty and are not automatically committed.
  • Recovered linked saved-query tabs use the existing external conflict/deletion behaviour.
  • Dashboard-variable tabs retain their binding and current Save semantics.
  • OAuth failure preserves the tab payload; retry rebinds it to the newly generated OAuth state.
  • A stale/mismatched callback cannot consume or restore the snapshot.
  • A snapshot is not applied to another workspace.
  • Malformed, unsupported, expired, or workspace-mismatched snapshots fail closed and are cleared.
  • The snapshot is removed only after successful restoration, and explicit Log out clears it.
  • Clean sessions continue through OAuth without unnecessary recovery state.
  • Recovery and the existing oauth_shared path have deterministic precedence and are not merged.
  • Unit tests cover snapshot encode/decode, invalid Spec text, tab-id allocation, storage failure, workspace mismatch, expiry, callback mismatch, retry state replacement, restore reconciliation, and one-shot unload suppression.
  • E2E coverage reproduces the dirty-tab expiry flow and verifies successful re-authentication restores the draft without an unload warning.
  • CHANGELOG.md and relevant authentication/session documentation are updated.

Related: #343, #466.

Metadata

Metadata

Assignees

No one assigned

    Labels

    inboxFiled mid-task; not yet triaged into the roadmap

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions