Skip to content

clickhouse-client --login=device does not authenticate confidential OAuth clients #1822

@Selfeer

Description

@Selfeer

Summary

When clickhouse-client is started with --login=device and an OAuth
credentials file whose client_id refers to a confidential client (one
the OpenID Connect provider has registered with client_secret authentication
required), the client never actually transmits the configured client_secret
on the RFC 8628 device authorization request.

The provider therefore answers the very first call — the one that should hand
back a device_code / user_code — with invalid_client / HTTP 401, and the
client aborts before any user interaction is possible:

Code: 516. DB::Exception: Device authorization request failed:
  Invalid client or Invalid client credentials. (AUTHENTICATION_FAILED)

The behaviour is identical whether the credentials file contains the correct
client_secret, a wrong one, or no secret at all. That is the giveaway: a
correct secret cannot possibly be wrong, so the only explanation is that the
secret never reaches the wire on the device-authorization POST.

Net effect: --login=device is unusable against any confidential OAuth
client
, regardless of provider. Only public/native clients work today.

Steps to reproduce

A reproducible setup needs a confidential OAuth client whose IdP enforces
client_secret authentication on the device-authorization endpoint. Any
RFC-8628-compliant IdP works — Keycloak is used below because it is
self-contained and free.

1. Start a Keycloak instance with a confidential client that has the device grant enabled

docker run -d --name kc -p 8080:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:26.3 start-dev --hostname=localhost

Wait until http://localhost:8080/health/ready returns 200, then create a
realm demo, a confidential client demo-confidential, and enable the
device grant. Either click through the admin UI (Clients → Create →
"Client authentication: ON", "OAuth 2.0 Device Authorization Grant Enabled:
ON") or POST equivalent JSON via the admin API. After creation, copy the
client secret from Credentials → Client secret; call it ${SECRET} below.

Add a test user (Users → Add user → username demo, set password demo,
mark Email Verified).

2. Sanity-check the IdP — the secret works when it actually reaches the wire

curl -s -o - -w '\nHTTP %{http_code}\n' \
  -X POST http://localhost:8080/realms/demo/protocol/openid-connect/auth/device \
  -d "client_id=demo-confidential" \
  -d "client_secret=${SECRET}" \
  -d "scope=openid"

Expected: HTTP 200 and a JSON body containing device_code, user_code,
verification_uri, verification_uri_complete, expires_in, interval.
This proves the realm, client, and secret are correctly configured.

3. Write a credentials file for clickhouse-client with the same correct secret

mkdir -p ~/.clickhouse-client
cat > ~/.clickhouse-client/oauth_client.json <<EOF
{
  "installed": {
    "client_id": "demo-confidential",
    "client_secret": "${SECRET}",
    "auth_uri":   "http://localhost:8080/realms/demo/protocol/openid-connect/auth",
    "token_uri":  "http://localhost:8080/realms/demo/protocol/openid-connect/token",
    "device_authorization_uri":
      "http://localhost:8080/realms/demo/protocol/openid-connect/auth/device"
  }
}
EOF
chmod 600 ~/.clickhouse-client/oauth_client.json

4. Run clickhouse-client --login=device

clickhouse-client --host <some-clickhouse-host> --login=device \
    --oauth-credentials ~/.clickhouse-client/oauth_client.json \
    --query 'SELECT 1'

Actual result

The client exits non-zero immediately, before printing any user_code /
verification URL:

Warning: OAuth credentials field 'token_uri' uses plain HTTP (...)
Warning: OAuth credentials field 'auth_uri' uses plain HTTP (...)
Warning: OAuth credentials field 'device_authorization_uri' uses plain HTTP (...)
Code: 516. DB::Exception: Device authorization request failed:
  Invalid client or Invalid client credentials. (AUTHENTICATION_FAILED)

Step 2 above (a direct curl with the same client_id + client_secret)
returns HTTP 200 and a normal device-code response. The IdP, the realm, the
client, the secret, and network reachability are all good — only
clickhouse-client cannot complete the device-authorization request.

The same error appears if the credentials file's client_secret is set to a
deliberately wrong string, or removed entirely. From the IdP's perspective
the three cases are indistinguishable, which is consistent with the secret
not being included in the request body at all.

Expected result

Per RFC 8628 §3.1, a confidential client MUST authenticate on the device
authorization request the same way it would on the token endpoint
(RFC 6749 §2.3 — typically client_secret_basic or client_secret_post).
With a correct client_secret:

  1. The device authorization request SHALL succeed; clickhouse-client SHALL
    print the user_code and verification URI to the terminal.
  2. After the user approves the code at the IdP, the polling token request
    SHALL succeed and the subsequent ClickHouse query SHALL execute under the
    authenticated identity.

With a missing or wrong client_secret, the existing invalid_client
diagnostic SHALL continue to surface with non-zero exit (already correct
today, just for the wrong reason — the secret is absent on the wire either
way).

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions