Skip to content

feat(qwp): refreshing token providers for Entra ID authentication - #102

Draft
bluestreak01 wants to merge 3 commits into
mainfrom
feat/qwp-entra-token-provider
Draft

bluestreak01 wants to merge 3 commits into
mainfrom
feat/qwp-entra-token-provider

Conversation

@bluestreak01

Copy link
Copy Markdown
Member

What this is

Support for QWP clients that authenticate with rotating bearer tokens: Microsoft Entra ID managed identities and service principals, typically obtained through DefaultAzureCredential. No QuestDB credentials are stored.

Today a static token= is captured once. Once it expires, every reconnect presents the dead token. The existing HttpTokenProvider hook already re-pulls a token on every connect round. What's missing is an expiry-aware shared cache, failure classification, a refresh signal on 401, and a connect-string way to select a provider.

Status: draft, spec only

This PR currently contains the design. The Java implementation follows in the steps below.

  • design/qwp-token-provider-spec.md: the cross-language specification (v0.3, all decisions resolved). Other clients (Rust/Python, Go, .NET, Node) implement this document.
  • design/entra-id-qwp-auth.md: findings for the Java client (with code references), the design rationale, and the Java implementation plan (§10).

Plan (design doc §10; test IDs refer to spec §10)

  • 1. Token cache: ExpiringToken, TokenSource, RefreshingTokenProvider, TokenUnavailableException. Tests C1–C8, C20.
  • 2. Client integration: one retry after a 401, SYNC startup classification, onTokenRejected. Tests C9–C16, C23.
  • 3. Connect-string keys (token_provider, azure_resource, azure_client_id), the shared registry, and the questdb-client-azure module. Tests C17–C19.
  • 4. Connection health and the optional authentication-outage deadline. Tests C21–C22.

Separate tickets, not in this PR:

  • a suspected dead pooled egress worker;
  • treating a 503 at the WebSocket upgrade as transient in every phase.

Security-relevant server findings were reported privately (see SECURITY.md) and are not part of this PR.

Add a cross-language specification for QWP clients that authenticate
with rotating bearer tokens (Microsoft Entra ID managed identities and
service principals), plus the Java findings and implementation plan.

- design/qwp-token-provider-spec.md (v0.3, decisions resolved): the
  token-source contract, a proactively refreshing shared token cache,
  when clients fetch tokens, failure policy by phase (one retry after
  401), connect-string keys (token_provider, azure_resource,
  azure_client_id), connection health, an optional authentication-
  outage deadline, redaction rules and conformance tests C1-C23.
- design/entra-id-qwp-auth.md: findings for the Java client with code
  references, the design rationale, and the four-step Java plan (§10).

Security-relevant server findings are reported privately and are not
part of this change.
AzureTokenProviderFactory factory = new AzureTokenProviderFactory();
Map<String, String> params = new HashMap<>();
params.put(TokenProviderSpec.KEY_AZURE_RESOURCE, "api://qdb");
params.put(TokenProviderSpec.KEY_AZURE_CLIENT_ID, "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d");

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.

🛑 Gitleaks has detected a secret with rule-id generic-api-key in commit 576383b.
If this secret is a true positive, please rotate the secret ASAP.

If this secret is a false positive, you can add the fingerprint below to your .gitleaksignore file and commit the change to this branch.

echo 576383b498f6ab57f6d34fae2e696f2f8210ae08:azure/src/test/java/io/questdb/client/azure/test/AzureTokenSourceTest.java:generic-api-key:137 >> .gitleaksignore

Implement the dynamic-credential specification
(design/qwp-token-provider-spec.md, v0.3) and the four-step Java plan in
design/entra-id-qwp-auth.md, section 10.

- Token cache: RefreshingTokenProvider, ExpiringToken, TokenSource and
  TokenUnavailableException. Proactive jittered refresh at about half the
  token lifetime, a 60 s hand-out floor, single-flight cold waits bounded
  by cold_wait, jittered backoff honouring Retry-After, rate-limited
  forced refresh, and token redaction everywhere.
- Client integration: one immediate same-endpoint retry after a
  refreshable 401 (WWW-Authenticate aware; never for a 403 or a static
  credential) on every ingest connect path, orphan drains included, and
  on egress connect and failover. SYNC startup retries a retryable
  provider failure within its budget (D6); any other provider exception
  still fails fast (D8). Egress errors name the failure class, and a
  query client whose failover reconnect failed reconnects on its next
  execute() instead of staying unusable (the suspected dead pooled
  worker, reproduced by a test before the fix).
- Connect string: token_provider, azure_resource and azure_client_id on
  both clients (wss:: only; exclusive with static credentials and
  application-supplied providers), resolved through a ServiceLoader SPI
  and a process-wide ref-counted registry with a 60 s linger. Validation
  never fetches a token.
- New optional module azure/ (org.questdb:questdb-client-azure):
  token_provider=azure on DefaultAzureCredential with the spec's error
  classification. Java 8 floor, released together with the client.
- Connection health: Sender.health(), QwpQueryClient.health() and an
  aggregate QuestDB.health(); an optional
  auth_failure_max_duration_millis deadline for authentication outages.
- Tests for conformance scenarios C1-C23, plus a TLS mode for the test
  WebSocket server (its key is generated at test time). README and design
  docs updated; the spec's Appendix B now names the published artifact
  org.questdb:questdb-client-azure.
@bluestreak01
bluestreak01 force-pushed the feat/qwp-entra-token-provider branch from 576383b to c9f2b96 Compare October 2, 2026 10:15
The spec classified Azure Identity's "no credential available in the
chain" as permanent (section 7.5), while section 4 calls network
failures and IMDS 404/410 retryable. Inside DefaultAzureCredential an
unreachable IMDS produces exactly that message: the chain probes IMDS
once, with a short timeout and no retries, so a transient IMDS outage
failed a SYNC startup fast.

Spec v0.4 follows Azure Identity's own split between fail-fast
discovery and a resilient single credential:

- 7.1, 7.2: a new key, azure_credential (default, managed_identity,
  workload_identity, environment), with its validation rules.
- 7.5: library errors are classified by how the credential was
  selected. With one credential, configuration errors are permanent
  and endpoint or network failures retryable. In the discovery chain,
  "no credential available" is retryable and the client warns once.
  Errors should carry the library's innermost reason.
- 4: a source that discovers its credential by probing must not treat
  "nothing found" as permanent on that evidence alone.
- 10: conformance test C24. 12: decision D10, which records why an
  SMBIOS host check was rejected. Appendix B: the Java binding.
  Appendix D: precedents from Azure Identity, MongoDB, Google, AWS
  and Apache Druid.

The Java implementation still follows v0.3.
@mtopolnik

Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 1210 / 1356 (89.23%)

file detail

path covered line new line coverage
🔵 io/questdb/client/QuestDB.java 0 1 00.00%
🔵 io/questdb/client/cutlass/auth/TokenProviderFactory.java 1 2 50.00%
🔵 io/questdb/client/cutlass/auth/TokenUnavailableException.java 11 15 73.33%
🔵 io/questdb/client/cutlass/auth/TokenProviderRegistry.java 84 112 75.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpWebSocketSender.java 64 75 85.33%
🔵 io/questdb/client/impl/SenderPool.java 6 7 85.71%
🔵 io/questdb/client/Sender.java 153 179 85.47%
🔵 io/questdb/client/ConnectionHealth.java 60 69 86.96%
🔵 io/questdb/client/cutlass/qwp/client/QwpQueryClient.java 92 104 88.46%
🔵 io/questdb/client/cutlass/qwp/client/QwpConnectionHealthTracker.java 67 75 89.33%
🔵 io/questdb/client/cutlass/http/client/WebSocketClient.java 21 23 91.30%
🔵 io/questdb/client/cutlass/auth/RefreshingTokenProvider.java 358 390 91.79%
🔵 io/questdb/client/cutlass/auth/TokenProviderSpec.java 78 83 93.98%
🔵 io/questdb/client/cutlass/auth/ExpiringToken.java 31 33 93.94%
🔵 io/questdb/client/cutlass/auth/CredentialRedaction.java 38 40 95.00%
🔵 io/questdb/client/cutlass/http/BearerChallenge.java 55 56 98.21%
🔵 io/questdb/client/cutlass/qwp/client/sf/cursor/CursorWebSocketSendLoop.java 53 54 98.15%
🔵 io/questdb/client/impl/PooledSender.java 1 1 100.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpCredentialUnavailableException.java 4 4 100.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpAuthFailedException.java 15 15 100.00%
🔵 io/questdb/client/QuestDBBuilder.java 2 2 100.00%
🔵 io/questdb/client/impl/ConfigSchema.java 4 4 100.00%
🔵 io/questdb/client/impl/QueryClientPool.java 5 5 100.00%
🔵 io/questdb/client/impl/QuestDBImpl.java 4 4 100.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpUpgradeFailures.java 2 2 100.00%
🔵 io/questdb/client/HttpTokenProvider.java 1 1 100.00%

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