Skip to content

Configure clickhouse-client OAuth login via connection block + CLI flags#1698

Open
BorisTyshkevich wants to merge 3 commits intoantalya-26.1from
pr3-oauth-via-connection-block
Open

Configure clickhouse-client OAuth login via connection block + CLI flags#1698
BorisTyshkevich wants to merge 3 commits intoantalya-26.1from
pr3-oauth-via-connection-block

Conversation

@BorisTyshkevich
Copy link
Copy Markdown
Collaborator

Summary

Replaces the oauth_client.json credentials file with first-class <oauth-*> fields in <connections_credentials>/<connection> blocks and matching --oauth-* CLI flags. Both sources merge into the same config layer; CLI wins on conflict (standard --user / --port precedence).

<connection>
    <name>my-server</name>
    <hostname>db.example.com</hostname>
    <port>9440</port><secure>1</secure>
    <login>browser</login>                               <!-- or device, or empty for cloud -->
    <oauth-url>https://idp.example.com</oauth-url>
    <oauth-client-id>...</oauth-client-id>
    <oauth-audience>...</oauth-audience>                 <!-- per IdP -->
    <oauth-client-secret>...</oauth-client-secret>       <!-- optional -->
    <oauth-callback-port>49152</oauth-callback-port>     <!-- pin for non-RFC-8252 IdPs -->
</connection>

CLI parity:

XML field CLI flag
<login> --login=browser|device|""
<oauth-url> --oauth-url
<oauth-client-id> --oauth-client-id
<oauth-audience> --oauth-audience
<oauth-client-secret> --oauth-client-secret
<oauth-callback-port> --oauth-callback-port

What's gone

  • loadOAuthCredentials() and oauth_client.json. Replaced by config/CLI fields plus full OIDC discovery.
  • --oauth-credentials CLI flag.
  • gtest_oauth_login.cpp's JSON-loader test cases (kept the PKCE / base64 cases).

What's new

  • Full OIDC endpoint discovery. Previously only the device endpoint was discovered from <issuer>/.well-known/openid-configuration; now authorization_endpoint, token_endpoint, and device_authorization_endpoint all auto-fill, so <oauth-url> alone is enough for any compliant OIDC IdP. OAuthProviderPolicy is refactored to share the discovery-document fetch and exposes populateEndpointsFromOIDCDiscovery().
  • <oauth-callback-port> field. Default 0 = "any port" (RFC 8252 §7.3 ephemeral binding) — works only with §7.3-compliant IdPs (currently Google). Non-zero pins the loopback port for IdPs that do literal-string match on the registered callback URL (Auth0). The doc covers this contract explicitly.
  • Restyled auth-code success page (centered card, light/dark scheme, brand line) so the post-redirect tab is unmistakable instead of <html><body>Authentication successful.</body></html>.

Docs

  • New page docs/en/interfaces/cli-oauth-login.md covering modes, per-provider notes (Auth0 / Google / Keycloak / Entra ID), server-side cross-reference to JWT validation, and the device-flow security considerations (RFC 8628 §5.3 Remote Phishing) explicitly noting the flow's suitability for testing/public services and unsuitability for sensitive DBMS access.
  • cli.md updated to point at the new doc; obsolete "OAuth credentials file" section removed.

Migration

Users of --login=device --oauth-credentials path/to/oauth_client.json:

// old: oauth_client.json
{ "installed": {
  "client_id": "X", "client_secret": "Y",
  "auth_uri": "...", "token_uri": "..." } }

becomes

<!-- new: ~/.clickhouse-client/config.xml -->
<connection>
    <name>my-server</name> <hostname>...</hostname>
    <login>device</login>
    <oauth-url>https://idp.example.com</oauth-url>
    <oauth-client-id>X</oauth-client-id>
    <oauth-client-secret>Y</oauth-client-secret>     <!-- only if IdP requires -->
</connection>

Refresh-token cache (~/.clickhouse-client/oauth_cache.json) format is unchanged; existing cached tokens keep working across the migration.

Dependencies

Test plan

  • Build clean against antalya-26.1
  • tests/queries/0_stateless/03749_cloud_endpoint_auth_precedence.sh — assertions updated for the new error messages, tests 9/10/11 pass locally
  • End-to-end: clickhouse client --connection antalya -q "SELECT 1" against Auth0 with <login>browser</login> + <oauth-callback-port>49152</oauth-callback-port> — auth code flow completes, browser shows the new success page, query returns
  • End-to-end: <login>device</login> against Auth0 — device code printed, post-approval token obtained, query returns
  • CLI overrides connection block (e.g. --oauth-callback-port=49153 overrides <oauth-callback-port>49152</oauth-callback-port>)
  • Reviewer: try Google IdP with <login>browser</login> and default <oauth-callback-port> (= 0); expect kernel-picks port to work since Google honors RFC 8252 §7.3
  • Reviewer: confirm there are no other entry points reading oauth_client.json (grep is clean: 0 hits in src/ after this PR)

🤖 Generated with Claude Code

Replaces the oauth_client.json credentials file with first-class
<oauth-*> fields in <connections_credentials>/<connection> blocks and
matching --oauth-* command-line flags. Both sources merge into the same
config layer; CLI wins on conflict (standard --user / --port behavior).

Connection block / CLI fields:

  <login>browser|device|""</login>            --login=browser|device|""
  <oauth-url>...</oauth-url>                  --oauth-url=...
  <oauth-client-id>...</oauth-client-id>      --oauth-client-id=...
  <oauth-audience>...</oauth-audience>        --oauth-audience=...
  <oauth-client-secret>...</oauth-client-secret>  --oauth-client-secret=...
  <oauth-callback-port>...</oauth-callback-port>  --oauth-callback-port=...

Bare <login></login> (or bare --login) keeps the existing ClickHouse
Cloud auto-login path. Explicit browser/device modes require
oauth-url + oauth-client-id and run via the OAuthLogin id-token path
that PR #1606 introduced for --login=device.

Endpoint discovery: when <oauth-url> is set the client fetches
<issuer>/.well-known/openid-configuration and fills authorization /
token / device_authorization endpoints. Previously OIDC discovery
only filled the device endpoint; OAuthProviderPolicy is refactored to
share the discovery-document fetch and to publish a public
populateEndpointsFromOIDCDiscovery() helper.

Browser-flow loopback port: <oauth-callback-port> defaults to 0
("any port"; kernel picks an ephemeral port at flow start). 0 is
RFC 8252 §7.3 semantics — only IdPs that honor the §7.3 port-wildcard
rule for loopback redirects accept it (currently Google). Auth0 does
literal-string match on registered callback URLs and rejects any
unregistered port; users with Auth0 must pin a non-zero port and
register http://127.0.0.1:<port>/callback. The doc page covers this
explicitly.

Removed:
- loadOAuthCredentials() and oauth_client.json. The Google-format JSON
  loader is replaced by config/CLI fields plus OIDC discovery; the JSON
  file is no longer read or referenced anywhere.
- --oauth-credentials CLI flag. Same.
- gtest_oauth_login.cpp's JSON-loader test cases (kept the PKCE / base64
  cases, which test the auth-code flow primitives).

Auth-code flow success page is restyled (centered card, light/dark
scheme, brand line) so the post-redirect tab is unmistakable instead
of `<html><body>Authentication successful.</body></html>`.

Docs:
- New page docs/en/interfaces/cli-oauth-login.md covering modes,
  per-provider notes (Auth0, Google, Keycloak/Entra), server-side
  cross-reference to JWT validation, and the device-flow security
  considerations (RFC 8628 §5.3 Remote Phishing) explicitly noting
  the flow's suitability for testing/public services and unsuitability
  for sensitive DBMS access.
- cli.md updated to point at the new doc and the obsolete
  "OAuth credentials file" section is removed.

Stateless test 03749_cloud_endpoint_auth_precedence.sh updated for the
new error messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Boris Tyshkevich <btyshkevich@altinity.com>
parseConnectionsCredentials parsed the field into
ConnectionsCredentials::oauth_callback_port, but Client::initialize
forgot to mirror it into config() the way it does for the other
<oauth-*> fields. The CLI plumbing went through correctly via
config().setUInt(...), but the connection-block path silently dropped
the value, so the loopback server kept binding port 0 (kernel-picks)
and Auth0 rejected the unregistered port.

Add the missing configuration.setUInt mirror, parallel to
oauth-url / oauth-client-id / oauth-audience / oauth-client-secret.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Boris Tyshkevich <btyshkevich@altinity.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Workflow [PR], commit [8a30e80]

Signed-off-by: Boris Tyshkevich <btyshkevich@altinity.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant