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.
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_verifierauthenticates the token exchange, so noclient_secretis needed andconfig.jsonstays secret-free. Supported by most OIDC providers. - Web client that requires a secret. Some providers (e.g. a Google "Web
application" client) require
client_secreton the in-browser token exchange even with PKCE. The code acceptsclient_secretinconfig.jsonfor this case. Since it ships to browsers, pair it with a redirect URI locked to exactlyhttps://<host>/sqland 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.jsoncarries 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.
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.
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.
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):
{}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:9000opens 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.xmlsetsconnect-src 'self'(+ the IdP origins). The browser will block a query POST to any other origin until you add that origin toconnect-src— otherwise the request never leaves the page. - The target ClickHouse must allow CORS for this origin: answer the
Authorization-header preflight (OPTIONS) and returnAccess-Control-Allow-Origin. ClickHouse'sadd_http_cors_headercovers 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 dropdown above the login card is only ever populated
by local development — npm 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.
- README "Installing on any ClickHouse cluster"
— the installer that renders
config.jsonand wires up ClickHouse. - docs/CLICKHOUSE-OAUTH.md / docs/CLICKHOUSE-OSS-OAUTH.md
— the ClickHouse-side JWT verification wiring (
<token_processors>or a delegated verifier) that aconfig.jsonissuer must pair with. - SECURITY.md — the full threat model for why
config.jsonis public, the redirect-lock requirement, and token storage.