Skip to content

feat(auth): JWT support for the HTTP client and Cloud JWT tests - #649

Open
polyglotAI-bot wants to merge 2 commits into
mainfrom
polyglot/jwt-bearer-auth
Open

polyglotAI-bot wants to merge 2 commits into
mainfrom
polyglot/jwt-bearer-auth

Conversation

@polyglotAI-bot

@polyglotAI-bot polyglotAI-bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Completes JWT (bearer token) authentication in the HTTP client, and makes the Cloud JWT tests run again with a new token for each run. The test setup follows ClickHouse/clickhouse-java#3099.

The client could already send Authorization: Bearer (ClickHouseClientSettings.BearerToken, QueryOptions.BearerToken, ClickHouseCommand.BearerToken). These parts were missing:

  • No connection string key. It was not possible to set a token in a connection string. Also, ClickHouseConnection.ConnectionString and ClickHouseDataSource.ConnectionString dropped a token that was set in the settings, so a connection made again from that string used Basic authentication.
  • No working tests. BearerAuthenticationTests were [Ignore]d (Temporarily disable BearerAuthenticationTests #359). The Cloud job used a static JWT secret, and that token expires.
  • No docs. docs/http.mdx did not describe JWT authentication.

Changes

Client

  • ClickHouseConnectionStringBuilder.BearerToken: new BearerToken connection string key. Both conversions now map it (FromSettings and ClickHouseClientSettings.FromConnectionStringBuilder). PublicAPI.Unshipped.txt is updated.
  • The token is masked as **** in ClickHouseClientSettings.ToString() and in the db.connection_string OpenTelemetry tag (RedactedConnectionString). Without this mask, the new mapping would put the token in that tag in clear text.

CI

  • .github/scripts/generate_jwt.py signs an RS256 JWT (iss=mydomain.com, aud=ci-test-service, sub=ci-test, valid for 30 minutes) with the private key in JWT_PKEY. These are the claim values that clickhouse-java uses.
  • .github/scripts/generate_jwks.py writes the public jwks.json for that key. It is the script from the JWT setup guide.
  • tests-cloud.yml:
    • A new Sign JWT step masks the token and gives it to the Test step as CLICKHOUSE_CLOUD_JWT. Only this step gets JWT_PKEY; the dotnet test process never gets the private key.
    • If the secret is not set, the step shows a warning and the JWT tests are skipped.
    • The reference to INTEGRATIONS_TEAM_TESTS_CLOUD_JWT_DESERT_VM_43 is removed.
  • The Python dependencies are pinned with hashes (requirements.in → requirements.txt, installed with --require-hashes), because they install in a job that has the Cloud secrets. Dependabot gets a pip entry for them.
  • tests.yml: .github/scripts/** is added to the path filters, so that a change to only the scripts starts the Cloud job.

Docs

  • docs/http.mdx: a new "JWT authentication" section, and a BearerToken row in the connection settings table.
  • changelog.d/649-jwt-bearer-auth.features.md.

Test

  • Cloud (BearerAuthenticationTests, skipped when there is no CLICKHOUSE_CLOUD_JWT): the [Ignore] is removed. New tests check that SELECT currentUser() matches ^JWT::.+::.+$, which proves that the server authenticated the request with the JWT, for:
    • ClickHouseClient with ClickHouseClientSettings.BearerToken;
    • ClickHouseConnection with the BearerToken connection string key;
    • a Basic-authenticated client with QueryOptions.BearerToken. Only that query runs as the JWT user; the next query runs as the Basic user again.
  • Unit tests:
    • The BearerToken key round-trips through the connection string, the settings, and back. The cases are a JWT and an opaque token with ;, = and ".
    • Without the key, the token stays null.
    • A connection made again from ClickHouseConnection.ConnectionString sends the token (header checked with TrackingHandler).
    • ClickHouseDataSource.ConnectionString contains the token.
    • ToString() and RedactedConnectionString mask the token.
  • I removed the BearerToken = builder.BearerToken mapping, and the round-trip and connection string tests failed. With the mapping, they pass.
  • Full HTTP suite on net10.0 against ClickHouse 26.9: 11081 passed, 0 failed. The changed files have 100% line coverage.
  • zizmor 1.29.0 (--config .github/zizmor.yml) reports no new findings at any severity.
  • The scripts were tested with a throwaway key: the token verifies against the generated JWKS, the kid values match, and the JWKS has no private key parameters.

Important

The Cloud JWT tests did not run in this PR, because the JWT_PKEY secret does not exist yet. The Cloud job only shows the "JWT tests skipped" warning. Please do the setup below, then run the Cloud job again before merge.

Cloud setup necessary for testing

These steps are also in .github/scripts/README.md. Do them outside of the repository directory, so that the private key cannot be committed. Add the secret last: when the secret is set, the job uses it, and the JWT tests fail until the provider accepts the tokens.

  1. Make the private key:
    openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out pkcs8_key.pem
  2. Make jwks.json, then commit and push it to this branch (polyglot/jwt-bearer-auth). It contains only the public key.
    python3 -m venv .venv
    .venv/bin/pip install --require-hashes -r <repo>/.github/scripts/requirements.txt
    export JWT_PKEY="$(cat pkcs8_key.pem)"
    .venv/bin/python <repo>/.github/scripts/generate_jwks.py > <repo>/.github/jwks.json
  3. In the ClickHouse Cloud console, open the service of the CLICKHOUSE_CLOUD_HOST secret. Go to Settings > Security > JWT authentication > Set up JWT providers, and add a provider:
    • Name: clickhouse-cs-ci (you cannot change it later)
    • Issuer: mydomain.com
    • Audience: ci-test-service
    • JWKS URL: https://raw.githubusercontent.com/ClickHouse/clickhouse-cs/polyglot/jwt-bearer-auth/.github/jwks.json. After the merge, change the URL to .../clickhouse-cs/main/.github/jwks.json.
    • Roles claim: empty
  4. Make sure that the service accepts a token. The result must be JWT::ci-test::<hash>.
    TOKEN="$(.venv/bin/python <repo>/.github/scripts/generate_jwt.py)"
    curl -H "Authorization: Bearer $TOKEN" "https://<CLICKHOUSE_CLOUD_HOST>:8443/?query=SELECT+currentUser()"
  5. Add the repository secret: gh secret set JWT_PKEY --repo ClickHouse/clickhouse-cs < pkcs8_key.pem
  6. Run the Cloud job again and make sure that BearerAuthenticationTests pass. Then delete pkcs8_key.pem, or keep it only in a secret store.

Notes:

  • Sharing with clickhouse-java. If the cs and java Cloud tests use the same service, the clickhouse-java provider and key can be used instead: set the same private key as JWT_PKEY, and skip steps 2 and 3. The claim values are the same.
  • Key safety. Keep JWT_PKEY as safe as CLICKHOUSE_CLOUD_PASSWORD. A person who has the key can sign a token with clickhouse:grants or clickhouse:roles claims, up to the provider's permission limit (the default user).
  • No grants needed. The CI tokens carry no grants, and the tests only run SELECT 1 and SELECT currentUser().

Not in this PR

  • Changing the client-level token at runtime. Add credential rotation #600 (CredentialsProvider) adds this for both Basic and Bearer credentials, so this PR does not add a second API for it. Until then, a new token can be passed per request with QueryOptions.BearerToken or ClickHouseCommand.BearerToken; the docs say this.
  • An empty per-query token. QueryOptions.BearerToken = "" falls back to Basic authentication, not to the client token. The cause is the ?? in AddDefaultHttpHeaders. This behavior existed before this PR, and this PR does not change it.
  • The native TCP client. It has no bearer token support. This PR changes only the HTTP client.

Checklist

  • Unit and integration tests covering the common scenarios were added
  • A human-readable description of the changes was provided to include in CHANGELOG

🤖 Generated with Claude Code

- Add the BearerToken connection string key. ClickHouseConnection.ConnectionString
  and ClickHouseDataSource.ConnectionString keep a token set in the settings.
- Mask the token in ClickHouseClientSettings.ToString and in the
  db.connection_string OpenTelemetry tag.
- Enable BearerAuthenticationTests again and check that a JWT request runs as
  the JWT::<sub>::<hash> ephemeral user (settings, connection string, per query).
- Cloud job: sign a new JWT for each run from the JWT_PKEY secret
  (.github/scripts/generate_jwt.py), as clickhouse-java#3099 does. The setup is
  in .github/scripts/README.md.
- Document JWT authentication in docs/http.mdx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 1, 2026 15:05
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The required public JWKS is absent and the Cloud JWT tests have not run successfully end to end.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds complete JWT bearer-token support to the HTTP client, including connection strings, redaction, documentation, and Cloud testing infrastructure.

Changes:

  • Adds BearerToken connection-string mapping and secret redaction.
  • Expands unit and Cloud JWT authentication coverage.
  • Adds short-lived JWT generation and pinned CI dependencies.
File Description
docs/​http.mdx Documents JWT authentication.
ClickHouse.Driver/​PublicAPI/​PublicAPI.Unshipped.txt Records the new public property.
ClickHouse.Driver/​ClickHouseClient.cs Redacts tokens from telemetry connection strings.
ClickHouse.Driver/​ADO/​ClickHouseConnectionStringBuilder.cs Adds and maps BearerToken.
ClickHouse.Driver/​ADO/​ClickHouseClientSettings.cs Parses, preserves, and masks tokens.
ClickHouse.Driver.Tests/​ClickHouseClientTests.cs Tests telemetry redaction.
ClickHouse.Driver.Tests/​ADO/​DataSourceTests.cs Tests data-source token preservation.
ClickHouse.Driver.Tests/​ADO/​ConnectionTests.cs Tests reapplied connection authentication.
ClickHouse.Driver.Tests/​ADO/​ConnectionStringBuilderTests.cs Tests token round trips.
ClickHouse.Driver.Tests/​ADO/​ClickHouseClientSettingsTests.cs Tests settings redaction.
ClickHouse.Driver.Tests/​ADO/​BearerAuthenticationTests.cs Re-enables and expands Cloud JWT tests.
changelog.d/​649-jwt-bearer-auth.features.md Adds the release-note fragment.
.github/​workflows/​tests.yml Includes JWT scripts in workflow paths.
.github/​workflows/​tests-cloud.yml Generates a token for Cloud tests.
.github/​scripts/​requirements.txt Locks Python dependencies with hashes.
.github/​scripts/​requirements.in Declares JWT script dependencies.
.github/​scripts/​README.md Documents Cloud JWT setup.
.github/​scripts/​generate_jwt.py Signs short-lived test JWTs.
.github/​scripts/​generate_jwks.py Generates the public JWKS.
.github/​scripts/​.gitignore Excludes local key artifacts.
.github/​dependabot.yml Enables Python dependency updates.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/scripts/README.md
| Part | Where | Secret? |
|---|---|---|
| RSA private key (PEM) | repository secret `JWT_PKEY` | yes |
| Public key set | `.github/jwks.json` in this repository (step 2 adds it) | no |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. jwks.json must come from the same private key that becomes the JWT_PKEY secret. That key must stay with a maintainer, so the agent that opened this PR did not make it. Step 2 of "Cloud setup necessary for testing" in the PR description tells how to make jwks.json and commit it to this branch, before the Cloud provider and the secret are set. The README table says "step 2 adds it".

Until the setup is done, the Cloud job skips the JWT tests and shows a warning. That is the result in this run.

@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
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.

2 participants