Skip to content

Latest commit

 

History

History
208 lines (173 loc) · 10.9 KB

File metadata and controls

208 lines (173 loc) · 10.9 KB

Configuring the login screen

The sign-in screen offers up to three sign-in paths at once — SSO, a ClickHouse® username/password, and a picker for connecting to a different server — so a first-time visitor can see more controls than their deployment actually needs (#123). All of it is a config.json choice; this page is the full reference for that config, and for what each part of the screen does.

Configuring OAuth

config.json carries the issuer, client_id, and optionally client_secret and audience. config.json is served to browsers, so the right shape depends on your IdP and threat model. Common, all valid, variants:

  • Public client + PKCE (no secret). Register a "SPA / public / native" client; the PKCE code_verifier authenticates the token exchange, so no client_secret is needed and config.json stays secret-free. Supported by most OIDC providers.
  • Web client that requires a secret. Some providers (e.g. a Google "Web application" client) require client_secret on the in-browser token exchange even with PKCE. The code accepts client_secret in config.json for this case. Since it ships to browsers, pair it with a redirect URI locked to exactly https://<host>/sql and a suitably scoped consent screen.
  • Broker server-side. Front the app with an OIDC broker / auth proxy that holds the provider secret and exposes a public PKCE client; the browser talks only to the broker and config.json carries no secret. More moving parts, keeps every provider secret off the browser.

The code treats client_secret as optional, so any of these is a config-only choice.

Reauthentication and unsaved work

Temporary ClickHouse authentication loss does not end the document session. The editor, tabs, drafts, navigation, dirty state, and completed results remain mounted while the same authentication controls appear in the application shell. A successful Basic sign-in resumes that exact in-memory session without a page reload; the inline form clears its password and resets password visibility after success so it is safe to reuse after another loss.

OAuth requires a page redirect. Before navigating, the browser writes a versioned recovery checkpoint to this tab's sessionStorage, but only when there is save-relevant dirty work. The checkpoint is bound to the OAuth attempt's random state and the current workspace id/key. It contains authored tab state (including SQL and raw Spec drafts and Dashboard-variable bindings), not OAuth tokens, passwords, query results, ClickHouse session ids, result columns, or running/export state. Like the OAuth token itself, sessionStorage is tab-scoped browser storage, not encryption: script running in the same origin can read it.

The intentional redirect bypasses the dirty-page warning exactly once, and only after that checkpoint write succeeds. If serialization or storage fails, the redirect does not start, the warning remains armed, and the inline controls show the failure so sign-in can be retried. A failed IdP callback keeps the draft checkpoint; a retry binds the same authored payload to its new OAuth state. A separate tab-scoped, TTL-bound validated-callback marker contains the callback state, its validation time, and a compact fingerprint of the authored document session at that point. The fingerprint is a change detector, not an authorization secret; the checkpoint alone never authorizes an automatic restore. Starting another OAuth attempt invalidates the older marker. Markers written by the previous version-1 format lack that fingerprint, so they are treated as unsupported and cleared rather than migrated; the retained checkpoint can be rebound by a later OAuth retry, which writes a version-2 marker.

After a successful callback, the app restores the route, loads the committed workspace, validates the checkpoint against that workspace and callback state, reconciles linked saved-query tabs, and reparses every raw Spec before the first signed-in render. Invalid JSON drafts survive as authored and regain normal diagnostics. If the workspace is unavailable, or storage/validation fails before publication, recovery remains unpublished and retryable. A valid pending recovery suppresses legacy shared content and retries automatically only after an authoritative workspace load while the document session is unchanged. If that session changed after callback validation, the checkpoint is retained and the user must choose Restore drafts explicitly; automatic recovery never overwrites newer work in memory. A workspace-mismatched checkpoint is likewise retained, with its callback authority, for a later matching workspace rather than discarded. Its marker is retired before publication. Results and other execution state do not survive an OAuth reload. Malformed, unsupported, or logically expired (after 15 minutes) checkpoints are ineligible; expiry does not promise eager physical removal from browser storage. A stale callback cannot consume a newer retry's checkpoint. Explicit Log out clears any pending checkpoint and validated-callback marker.

Multiple IdPs

config.json may instead list several providers, and the login screen shows one button per IdP ("Continue with …") rather than a single one:

{ "idps": [
    { "id": "google", "label": "Google",   "issuer": "https://accounts.google.com", "client_id": "" },
    { "id": "acme",   "label": "Acme SSO", "issuer": "https://acme.auth0.com",      "client_id": "", "client_secret": "" }
  ] }

Each entry takes the same fields as the single-IdP form (issuer, client_id, optional client_secret/audience/bearer/ch_auth/authorize_params) plus an optional id/label (default: the issuer host). A bare single object (above) is still accepted — it's treated as a one-IdP list. ClickHouse needs a matching <token_processor> per issuer; it validates each inbound JWT against whichever one matches the token's iss, so no extra CH wiring is required to offer several.

Credentials login (username / password)

Alongside SSO, the sign-in screen offers a ClickHouse username + password path (HTTP Basic), shown by default.

Hide it (SSO-only). If the cluster has no password-authenticated CH users — e.g. it only accepts JWTs via a token_processor/verifier — the credentials path would just 401, so set top-level "basic_login": false to drop it and offer SSO only. This is also the way to get the simplest possible screen — a single SSO button, with no credentials panel, Advanced disclosure, or Target row at all:

{
  "basic_login": false,
  "idps": [ { "id": "google", "issuer": "https://accounts.google.com", "client_id": "" } ]
}

(Some verifier setups do pass real CH password users through — e.g. a cluster with a demo user still accepts demo/password — so whether to hide the path is about what that server actually authenticates, not just "does it use OAuth".)

Credentials-only (no SSO). A deployment with no OAuth can omit idps entirely; the SSO buttons disappear and only the username/password form shows (basic_login defaults on):

{}

The host (Advanced → Server address)

Credentials authenticate against the serving host by default. The login screen's Advanced → Server address field can aim the credential path at a different host:port (a bare host defaults to https://…:8443); SSO always stays on the serving host. This disclosure is folded closed by default — it only opens when:

  • a ?host= URL param pre-fills it — e.g. …/sql?host=other.example:9000 opens Advanced with the address filled in and disables the SSO buttons (SSO can only target the serving host), so the link drops you straight into credential sign-in for that server; or
  • a saved-connection picker is populated (see below) and the user selects a plain/basic entry from it, which prefills the credentials form and opens Advanced.

So a standard hosted, single-cluster deployment (deploy/install.sh, no hosts in config.json, no ?host= links handed out) never shows this disclosure to its users — there's nothing to configure to hide it; it simply never opens.

A cross-origin target (a host other than the one serving the page) has two extra requirements:

  • The SPA's own CSP. deploy/http_handlers.xml sets connect-src 'self' (+ the IdP origins). The browser will block a query POST to any other origin until you add that origin to connect-src — otherwise the request never leaves the page.
  • The target ClickHouse must allow CORS for this origin: answer the Authorization-header preflight (OPTIONS) and return Access-Control-Allow-Origin. ClickHouse's add_http_cors_header covers the actual request; the preflight may need handler/proxy configuration on that server.

The password is held in sessionStorage for the tab session (same lifetime as the OAuth token) and sent as Authorization: Basic base64(user:password). A wrong password is surfaced on the login screen — the connect probe runs a SELECT 1 before entering the workbench.

The saved-connection picker (multi-host)

The Saved connection dropdown above the login card is only ever populated by local developmentnpm run local (or the installed local app) reads your ~/.clickhouse-client/config.xml connections into config.json's hosts list:

  • A plain connection (hostname/user/password) → selecting it prefills the credentials form (cross-origin HTTP Basic to that host) and opens Advanced.
  • A connection carrying clickhouse-client's OAuth keys (oauth-url, oauth-client-id, oauth-audience) → selecting it starts an OAuth sign-in against that cluster instead.

A connection with <accept-invalid-certificate>1</accept-invalid-certificate> (a self-signed or wrong-host TLS cert, common on dev tenants) is flagged in the picker. The browser refuses to fetch() such a host and JavaScript can't override that, so selecting it surfaces a one-time step: open the cluster in a new tab and accept its certificate, after which the SPA can reach it for the rest of the browser session. For an OAuth connection, the sign-in redirect is held behind a Continue button so the cert is trusted before any post-login query hits the cluster.

A hosted, single-cluster deployment doesn't set hosts, so its users never see this picker — it's purely a local/dev-tool feature, not something a production deployment needs to hide.

Reference