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:
- The device authorization request SHALL succeed;
clickhouse-client SHALL
print the user_code and verification URI to the terminal.
- 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).
Summary
When
clickhouse-clientis started with--login=deviceand an OAuthcredentials file whose
client_idrefers to a confidential client (onethe OpenID Connect provider has registered with
client_secretauthenticationrequired), the client never actually transmits the configured
client_secreton 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— withinvalid_client/ HTTP 401, and theclient aborts before any user interaction is possible:
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: acorrect 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=deviceis unusable against any confidential OAuthclient, regardless of provider. Only public/native clients work today.
Steps to reproduce
A reproducible setup needs a confidential OAuth client whose IdP enforces
client_secretauthentication on the device-authorization endpoint. AnyRFC-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
Wait until
http://localhost:8080/health/readyreturns 200, then create arealm
demo, a confidential clientdemo-confidential, and enable thedevice 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 passworddemo,mark
Email Verified).2. Sanity-check the IdP — the secret works when it actually reaches the wire
Expected:
HTTP 200and a JSON body containingdevice_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-clientwith the same correct secret4. Run
clickhouse-client --login=deviceActual result
The client exits non-zero immediately, before printing any
user_code/verification URL:
Step 2 above (a direct
curlwith the sameclient_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-clientcannot complete the device-authorization request.The same error appears if the credentials file's
client_secretis set to adeliberately 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_basicorclient_secret_post).With a correct
client_secret:clickhouse-clientSHALLprint the
user_codeand verification URI to the terminal.SHALL succeed and the subsequent ClickHouse query SHALL execute under the
authenticated identity.
With a missing or wrong
client_secret, the existinginvalid_clientdiagnostic 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).