From 5e9da21ff37f5b3a0b8ca96a3f763c9572923985 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 15:34:12 +0100 Subject: [PATCH 01/38] Document the missing OIDC configuration options Compared the `acl.oidc.*` keys in EntPropertyKey against the OIDC configuration page. Five options were missing and one was documented under a name the server does not recognise. Added: - `acl.oidc.state.required` - `acl.oidc.device.authorization.endpoint` - `acl.oidc.public.keys.expiry` - `acl.oidc.response.buffer.size` - `acl.oidc.string.pool.capacity` Corrected: - `acl.oidc.pkce.enabled` does not exist, the property is `acl.oidc.pkce.required` - `acl.oidc.groups.claim` has no default, it is mandatory when OIDC is enabled - setting both `acl.oidc.host` and `acl.oidc.configuration.url` fails server startup, they are mutually exclusive Also moved `acl.oidc.cache.ttl` out of the claims section into a new "Caching and buffers" section, next to the other caching and buffer settings. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 1 + documentation/configuration/oidc.md | 78 +++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index ca4f230c94..c044d9cfd3 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -48,6 +48,7 @@ This page tracks significant updates to the QuestDB documentation. - Added the [`cairo.sql.parquet.cache.memory.size`](/docs/configuration/cairo-engine/) configuration property (256 MB default), deprecating the slot-based `cairo.sql.parquet.frame.cache.capacity` - Documented [`cairo.root`](/docs/configuration/cairo-engine/) absolute-path behavior: the `conf`, `import`, `export`, `tmp`, and `.checkpoint` directories become siblings of the specified directory rather than children of the server root, so leave it at the default under Docker - [read_parquet](/docs/query/functions/parquet/#designated-timestamp) - Documented nominating a designated timestamp on a Parquet file with `TIMESTAMP()` (applied directly, on a sub-query, or on a CTE), and importing a file into a table with `INSERT INTO ... SELECT` or `CREATE TABLE AS` +- Documented the OIDC configuration options that were missing from the [OIDC page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.device.authorization.endpoint`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, and `acl.oidc.string.pool.capacity`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default ### Updated diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index e0c5932bdc..481322a568 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -55,8 +55,9 @@ configuration options must also be set. - **Default**: none - **Reloadable**: no -OIDC provider hostname. Required when OIDC is enabled, unless the OIDC -configuration URL is set. +OIDC provider hostname. Required when OIDC is enabled, unless +`acl.oidc.configuration.url` is set. The two are mutually exclusive, setting +both of them fails server startup. ### acl.oidc.http.timeout @@ -100,7 +101,7 @@ When enabled, the PGWire endpoint supports OIDC authentication. The OAuth2 token should be sent in the password field, while the username field should contain the string `_sso`, or left empty if that is an option. -### acl.oidc.pkce.enabled +### acl.oidc.pkce.required - **Default**: `true` - **Reloadable**: no @@ -116,6 +117,16 @@ be enabled in production. The Web Console is not fully secure without it. Enables or disables the Resource Owner Password Credentials flow. When enabled, this flow must also be configured in the OIDC Provider. +### acl.oidc.state.required + +- **Default**: `false` +- **Reloadable**: no + +Requires the `state` parameter in the Authorization Code Flow. The client +generates a random state value, which the OIDC Provider returns unchanged +together with the authorization code. Checking it protects against CSRF +attacks. Enable it if the Identity Provider supports the `state` parameter. + ## Endpoints ### acl.oidc.authorization.endpoint @@ -126,6 +137,17 @@ enabled, this flow must also be configured in the OIDC Provider. OIDC Authorization Endpoint. The default value should work for the Ping Identity Platform. +### acl.oidc.device.authorization.endpoint + +- **Default**: none +- **Reloadable**: no + +OIDC Device Authorization Endpoint, used by clients which authenticate with +the Device Code Flow. Unlike the other endpoints it has no default value. It +is resolved automatically if `acl.oidc.configuration.url` is set and the +OIDC Provider advertises a `device_authorization_endpoint`. The Device Code +Flow is unavailable if the endpoint is neither configured nor discovered. + ### acl.oidc.public.keys.endpoint - **Default**: `/pf/JWKS` @@ -196,22 +218,13 @@ which it connects. ## User and group claims -### acl.oidc.cache.ttl - -- **Default**: `30000` -- **Reloadable**: no - -User info cache entry TTL in milliseconds. QuestDB caches user info responses -for each valid access token. This setting controls how often the access token -is validated and user info refreshed. - ### acl.oidc.groups.claim -- **Default**: `groups` +- **Default**: none - **Reloadable**: no The name of the custom claim in the user information that contains the -group memberships of the user. +group memberships of the user. Required when OIDC is enabled. ### acl.oidc.groups.encoded.in.token @@ -230,3 +243,40 @@ group memberships directly into the token. The name of the claim in the user information that contains the user's name. Could be a username, full name, or email. Displayed in the Web Console and logged for audit purposes. + +## Caching and buffers + +### acl.oidc.cache.ttl + +- **Default**: `30000` +- **Reloadable**: no + +User info cache entry TTL in milliseconds. QuestDB caches user info responses +for each valid access token. This setting controls how often the access token +is validated and user info refreshed. + +### acl.oidc.public.keys.expiry + +- **Default**: `120000` +- **Reloadable**: no + +Expiry of the cached JSON Web Key Set (JWKS) in milliseconds. QuestDB caches +the public keys used to validate tokens issued by the OIDC Provider, and +reloads them from the public keys endpoint when the cache expires. + +### acl.oidc.response.buffer.size + +- **Default**: `1M` +- **Reloadable**: no + +Size of the buffer used to receive HTTP responses from the OIDC Provider. +Increase it if the provider sends large responses, such as user info +containing a long list of group memberships. + +### acl.oidc.string.pool.capacity + +- **Default**: `128` +- **Reloadable**: no + +Initial capacity of the string pool used when parsing JSON responses received +from the OIDC Provider. From dc7276c753619b6bee7555acabd17bec9334bff8 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 17:10:17 +0100 Subject: [PATCH 02/38] Note the state parameter requirement in the OIDC guide Some Identity Providers require the `state` parameter in the authorization request. Documented this in the Authentication and Authorization Flow walkthrough, next to the authorization code request, pointing at `acl.oidc.state.required`. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 1 + documentation/security/oidc.mdx | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index c044d9cfd3..e5167b7e55 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -49,6 +49,7 @@ This page tracks significant updates to the QuestDB documentation. - Documented [`cairo.root`](/docs/configuration/cairo-engine/) absolute-path behavior: the `conf`, `import`, `export`, `tmp`, and `.checkpoint` directories become siblings of the specified directory rather than children of the server root, so leave it at the default under Docker - [read_parquet](/docs/query/functions/parquet/#designated-timestamp) - Documented nominating a designated timestamp on a Parquet file with `TIMESTAMP()` (applied directly, on a sub-query, or on a CTE), and importing a file into a table with `INSERT INTO ... SELECT` or `CREATE TABLE AS` - Documented the OIDC configuration options that were missing from the [OIDC page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.device.authorization.endpoint`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, and `acl.oidc.string.pool.capacity`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default +- [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers ### Updated diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index ecd71b2953..0beb72944b 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -124,6 +124,21 @@ been authenticated already: https://oidc.provider:443/as/authorization.oauth2?client_id=questdb&response_type=code&scope=openid&redirect_uri=https%3A%2F%2Fquestdb.host%3A9000&code_challenge=IwZ-WuypAY3fMtvismbj1MQUe5CzMgrBa87nYcgFoLQ&code_challenge_method=S256 ``` +:::note + +Some Identity Providers require the `state` parameter in the authorization +request. It is another random value generated by the client, which the OIDC +Provider returns unchanged together with the authorization code. Checking it +protects against CSRF attacks. + +If your provider requires it, set +[`acl.oidc.state.required`](/docs/configuration/oidc/#acloidcstaterequired) to +`true`. The [Web Console](/docs/getting-started/web-console/overview/) then +generates the `state` parameter, sends it in the authorization request, and +validates the value returned by the provider. + +::: + ### 2. Prove identity Next, the user must prove its identity. From 8724eceff978cb869c2d5e46e6a101aed46bc982 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:45:18 +0100 Subject: [PATCH 03/38] Document the OIDC constraints which fail server startup The audit of `acl.oidc.*` covered defaults but not the validation which runs alongside them in EntPropServerConfiguration. Four constraints abort startup and none of them were documented: - OIDC and the Basic Auth Realm cannot both be enabled - the keystore path and password must be set together, or neither - `acl.oidc.tls.enabled` must match the scheme of every provider URL, including the endpoints discovered from the configuration document - `acl.oidc.enabled` needs `acl.oidc.client.id`, `acl.oidc.groups.claim` and one of `acl.oidc.host` / `acl.oidc.configuration.url` The last one replaces "several other configuration options must also be set", which left the reader to reconstruct the mandatory set by reading all 29 entries. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/iam.md | 4 ++++ documentation/configuration/oidc.md | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/documentation/configuration/iam.md b/documentation/configuration/iam.md index 7829c04982..e3f3b22354 100644 --- a/documentation/configuration/iam.md +++ b/documentation/configuration/iam.md @@ -47,6 +47,10 @@ Enables or disables the built-in admin user. When enabled, the browser's basic auth popup window is used instead of the Web Console's login screen. Present for backwards compatibility only. +Cannot be enabled together with +[`acl.oidc.enabled`](/docs/configuration/oidc/#acloidcenabled). Setting both +to `true` fails server startup. + ### acl.enabled - **Default**: `true` diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 481322a568..de73855353 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -47,8 +47,13 @@ JSON format. Should always end with `/.well-known/openid-configuration`. - **Default**: `false` - **Reloadable**: no -Enables or disables OIDC authentication. When enabled, several other -configuration options must also be set. +Enables or disables OIDC authentication. When enabled, `acl.oidc.client.id` +and `acl.oidc.groups.claim` must also be set, along with either +`acl.oidc.host` or `acl.oidc.configuration.url`. + +OIDC cannot be enabled together with +[`acl.basic.auth.realm.enabled`](/docs/configuration/iam/#aclbasicauthrealmenabled). +Setting both to `true` fails server startup. ### acl.oidc.host @@ -189,13 +194,17 @@ Whether the OIDC provider requires a secure connection. If the OpenID Provider endpoints do not require TLS, this can be set to `false`. This is unlikely in production. +This setting must match the scheme of every OIDC Provider URL QuestDB uses, +including `acl.oidc.configuration.url` and each endpoint discovered from it. +A URL whose scheme does not match fails server startup. + ### acl.oidc.tls.keystore.password - **Default**: none - **Reloadable**: no -Keystore password. Required if a keystore file is configured and is password -protected. +Keystore password. Must be set whenever `acl.oidc.tls.keystore.path` is set. +Setting either one without the other fails server startup. ### acl.oidc.tls.keystore.path From 5d31797869133eaa162cb85867f4608bf480ca03 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:45:33 +0100 Subject: [PATCH 04/38] Note that endpoint discovery supersedes the endpoint settings Setting `acl.oidc.configuration.url` makes the whole Endpoints section inert. PropOidcConfiguration only builds endpoints from the individual properties when no configuration URL is set, and init() then overwrites all five from the discovered document. `acl.oidc.port` goes the same way, as host and port come from the discovered URLs. The page implied the opposite, and only in the device authorization entry: "resolved automatically if acl.oidc.configuration.url is set" reads as though setting the property is the alternative to discovery. It is not, and the reader who tries it gets no log line saying so. State the rule under `acl.oidc.configuration.url`, where a reader meets it first, and again at the top of the Endpoints section. Also fixes the comma splice in the `acl.oidc.host` entry. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index de73855353..6ce8f5da6a 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -42,6 +42,14 @@ enabled. URL where the OpenID Provider's configuration information can be loaded in JSON format. Should always end with `/.well-known/openid-configuration`. +QuestDB downloads the document at startup and takes every endpoint from it, +so the settings under [Endpoints](#endpoints) and `acl.oidc.port` are not +used. The server does not start if the document cannot be downloaded or +parsed, or if it is missing the authorization, token, user info or JWKS +endpoint. + +Mutually exclusive with `acl.oidc.host`: setting both fails server startup. + ### acl.oidc.enabled - **Default**: `false` @@ -61,8 +69,8 @@ Setting both to `true` fails server startup. - **Reloadable**: no OIDC provider hostname. Required when OIDC is enabled, unless -`acl.oidc.configuration.url` is set. The two are mutually exclusive, setting -both of them fails server startup. +`acl.oidc.configuration.url` is set. The two are mutually exclusive: setting +both fails server startup. ### acl.oidc.http.timeout @@ -76,7 +84,8 @@ OIDC provider HTTP request timeout in milliseconds. - **Default**: `443` - **Reloadable**: no -OIDC provider port number. +OIDC provider port number. Not used when `acl.oidc.configuration.url` is +set, because the port is taken from the discovered endpoint URLs. ### acl.oidc.redirect.uri @@ -134,6 +143,10 @@ attacks. Enable it if the Identity Provider supports the `state` parameter. ## Endpoints +These settings apply only when the OIDC Provider is configured by host. When +`acl.oidc.configuration.url` is set, QuestDB takes every endpoint from the +provider's configuration document and the settings below are not used. + ### acl.oidc.authorization.endpoint - **Default**: `/as/authorization.oauth2` From 438f84eebe993a997231239a8d2878921846777e Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:46:21 +0100 Subject: [PATCH 05/38] Document the Device Code Flow in the OIDC guide `acl.oidc.device.authorization.endpoint` named a flow that appeared nowhere else in the docs. A reader who needed it found the knob and nothing else: no definition, no link, no explanation of who consumes the endpoint. The gap sat exactly where the flow belongs. The guide points CLI and standalone clients at the Resource Owner Password Credentials flow, which the same page flags as "legacy, and should be used as a last resort", and the client pages tell developers to acquire a token out-of-band without saying how. Describe the flow, state that QuestDB advertises the endpoint rather than running the flow, and cover both ways the endpoint is resolved. Add a tip in the CLI section so the reader is not steered to ROPC by default. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 1 + documentation/security/oidc.mdx | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index e5167b7e55..40e51f97be 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -50,6 +50,7 @@ This page tracks significant updates to the QuestDB documentation. - [read_parquet](/docs/query/functions/parquet/#designated-timestamp) - Documented nominating a designated timestamp on a Parquet file with `TIMESTAMP()` (applied directly, on a sub-query, or on a CTE), and importing a file into a table with `INSERT INTO ... SELECT` or `CREATE TABLE AS` - Documented the OIDC configuration options that were missing from the [OIDC page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.device.authorization.endpoint`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, and `acl.oidc.string.pool.capacity`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default - [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers +- [OpenID Connect (OIDC)](/docs/security/oidc/#device-code-flow) - Documented the Device Code Flow for clients without a browser of their own, how QuestDB advertises the Device Authorization Endpoint to them, and why it is preferable to the Resource Owner Password Credentials flow ### Updated diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 0beb72944b..a47eb034e9 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -533,6 +533,40 @@ testldap=> testldap=> ``` +:::tip + +The Resource Owner Password Credentials flow requires the client to handle the +user's password. A client which is able to display a URL and poll for the +result should use the [Device Code Flow](#device-code-flow) instead. + +::: + +### Device Code Flow + +The [Device Code Flow](https://datatracker.ietf.org/doc/html/rfc8628) is the +standard way for a client with no browser of its own to obtain tokens without +ever handling the user's password. The client asks the OIDC Provider for a +device code, displays a short user code together with a verification URL, and +polls the Token endpoint while the user completes the login in a browser on any +device. + +QuestDB does not run the flow itself. It publishes the OIDC Provider's Device +Authorization Endpoint to clients through its settings endpoint, so a client +knows where to start: + +- When `acl.oidc.configuration.url` is set, QuestDB takes the endpoint from the + provider's configuration document, provided the provider advertises a + `device_authorization_endpoint`. +- When the provider is configured by host, set + [`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint) + explicitly. It is the only endpoint with no default value. + +If QuestDB does not advertise the endpoint, the client has to be given it +directly, or resolve it from the OIDC Provider itself. + +Once the client holds an access token, it presents it to QuestDB the same way +as any other token. + ## Non-interactive clients Non-interactive clients are usually jobs or standalone applications, such as a From a890f52d3b02fa84f8476f3bf8a5e609e9544cb0 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:46:35 +0100 Subject: [PATCH 06/38] Correct what the device authorization endpoint setting controls Two claims in the entry did not hold. "The Device Code Flow is unavailable if the endpoint is neither configured nor discovered" describes a gate the server does not have. QuestDB has no Device Code Flow implementation; the endpoint's only use is exportConfiguration, which publishes it to clients. A client that knows its provider's device endpoint runs the flow either way, and QuestDB validates the resulting token as usual. "It is resolved automatically if acl.oidc.configuration.url is set" presented discovery as specific to this endpoint and implied the property is the fallback when a provider does not advertise one. Discovery covers all five endpoints, and in that mode this property is not read at all. The general rule now lives under `acl.oidc.configuration.url`, so the entry no longer needs to half-state it. Also links the flow to its new section in the guide, and adds the comma after the fronted adverbial. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 6ce8f5da6a..aab1c7091b 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -161,10 +161,13 @@ Identity Platform. - **Reloadable**: no OIDC Device Authorization Endpoint, used by clients which authenticate with -the Device Code Flow. Unlike the other endpoints it has no default value. It -is resolved automatically if `acl.oidc.configuration.url` is set and the -OIDC Provider advertises a `device_authorization_endpoint`. The Device Code -Flow is unavailable if the endpoint is neither configured nor discovered. +the [Device Code Flow](/docs/security/oidc/#device-code-flow). Unlike the +other endpoints, it has no default value. + +QuestDB does not run the Device Code Flow itself. It publishes this endpoint +to clients through the settings endpoint, so that a client knows where to +start the flow. A client which does not receive it has to be given the +endpoint directly, or resolve it from the OIDC Provider itself. ### acl.oidc.public.keys.endpoint From 6da5c5b9c92028c9556264b62f21fbf6773a471f Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:46:48 +0100 Subject: [PATCH 07/38] Distinguish the millisecond settings which accept a duration `acl.oidc.public.keys.expiry` is read with getMillis, so `2m` and `120s` work. `acl.oidc.cache.ttl` and `acl.oidc.http.timeout` are read with getInt, which rejects anything but digits and turns it into a ServerConfigurationException. All three said only "in milliseconds". Grouping cache.ttl and public.keys.expiry under the new "Caching and buffers" heading put the two adjacent, which makes the identical wording worse: `acl.oidc.public.keys.expiry=30s` starts, `acl.oidc.cache.ttl=30s` does not. While in the cache.ttl entry, document that `0` disables the user info cache. isOidcCacheEnabled() tests `cacheTtl > 0` and swaps in a no-op cache. That is the knob for making a revoked account take effect immediately, which the guide discusses without naming a value. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index aab1c7091b..debd7eec0d 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -77,7 +77,8 @@ both fails server startup. - **Default**: `30000` - **Reloadable**: no -OIDC provider HTTP request timeout in milliseconds. +OIDC provider HTTP request timeout in milliseconds. Accepts a plain integer +only. ### acl.oidc.port @@ -276,18 +277,25 @@ logged for audit purposes. - **Default**: `30000` - **Reloadable**: no -User info cache entry TTL in milliseconds. QuestDB caches user info responses -for each valid access token. This setting controls how often the access token -is validated and user info refreshed. +User info cache entry TTL in milliseconds, as a plain integer only. QuestDB +caches user info responses for each valid access token. This setting controls +how often the access token is validated and user info refreshed. + +Set it to `0` to disable the cache, so that every request is validated against +the OIDC Provider. ### acl.oidc.public.keys.expiry - **Default**: `120000` - **Reloadable**: no -Expiry of the cached JSON Web Key Set (JWKS) in milliseconds. QuestDB caches -the public keys used to validate tokens issued by the OIDC Provider, and -reloads them from the public keys endpoint when the cache expires. +Expiry of the cached JSON Web Key Set (JWKS) in milliseconds. Also accepts a +duration, such as `2m` or `120s`. + +QuestDB caches the public keys used to validate tokens issued by the OIDC +Provider, and reloads them from the public keys endpoint when the cache +expires. Lower it if the OIDC Provider rotates its signing keys frequently, at +the cost of more requests to the endpoint. ### acl.oidc.response.buffer.size From 97559b4e66db15b89d4cd70d844e9a7f587a56cf Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:47:01 +0100 Subject: [PATCH 08/38] Give one rule for enabling the state parameter The two pages disagreed on when to set `acl.oidc.state.required`. The reference page said to enable it if the provider "supports" the `state` parameter; the guide said to enable it if the provider "requires" it. Effectively every provider supports `state`, so the reference page's rule resolved to "always enable it", which its own default of `false` contradicts. Both rules are half the answer: enable it when the provider requires it, and enabling it is a reasonable defence in depth otherwise. Say that once, and drop the mechanism description the guide already carries. The entry also read as though the server enforces something. It does not - isStateRequired() has one consumer, exportConfiguration - so name the Web Console as the component which generates and checks the value, and link to the step in the guide that walks through it. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index debd7eec0d..c8ff2bb925 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -137,10 +137,16 @@ enabled, this flow must also be configured in the OIDC Provider. - **Default**: `false` - **Reloadable**: no -Requires the `state` parameter in the Authorization Code Flow. The client -generates a random state value, which the OIDC Provider returns unchanged -together with the authorization code. Checking it protects against CSRF -attacks. Enable it if the Identity Provider supports the `state` parameter. +Requires the `state` parameter in the Authorization Code Flow, as a defence +against CSRF attacks. QuestDB does not see the value itself; it publishes the +setting to clients through the settings endpoint, the same way it publishes +`acl.oidc.pkce.required`. + +Enable it if the OIDC Provider requires the `state` parameter, or to add CSRF +protection on top of PKCE. The +[Web Console](/docs/security/oidc/#1-secret-generation) generates the value, +sends it in the authorization request, and checks that the provider returns it +unchanged. ## Endpoints From 88ee167637248d1deffce63831a53a63020211c6 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:47:12 +0100 Subject: [PATCH 09/38] Correct what happens when the groups claim is missing The guide said the user "is authenticated, they have no permissions at all". There is no such state. OidcClientImpl.verifyTokenSlow returns false when the sub claim is empty or the groups list is empty, which propagates to a bare 401. The empty sub claim was not mentioned at all. The distinction matters when reading logs: a 401 sends an operator looking at tokens, clocks and JWKS, while the actual cause is a group claim that stopped populating on the provider side. The passage also said only that the claim name "is configurable in QuestDB" without naming the property. Now that `acl.oidc.groups.claim` is documented as mandatory with no default, name it and link it, so the guide and the reference page agree. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/security/oidc.mdx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index a47eb034e9..7c5a58248a 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -707,12 +707,14 @@ QuestDB works the list of external groups out from the User Info response message. If we take the example used earlier, we will see that the message contains a -claim called `groups`. This name is configurable in QuestDB. - -If the groups claim is missing or it is an empty list, the user cannot access -the database. - -Although the user is authenticated, they have no permissions at all. +claim called `groups`. Its name is set with +[`acl.oidc.groups.claim`](/docs/configuration/oidc/#acloidcgroupsclaim), which +has no default and must be set whenever OIDC is enabled. + +If the groups claim is missing or it is an empty list, authentication fails and +QuestDB replies with `401 Unauthorized`. The same happens when the claim named +by [`acl.oidc.sub.claim`](/docs/configuration/oidc/#acloidcsubclaim) is missing +or empty. The user has to have at least the `HTTP` permission to be able to successfully login via the [Web Console](/docs/getting-started/web-console/overview/). From 93dc7f7360f8172bff575ad843ecda21c5177694 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:47:55 +0100 Subject: [PATCH 10/38] Add the missing QuestDB configuration to the PingFederate guide The PingFederate walkthrough contains no `acl.oidc.*` settings at all. It ends with "To test, head to http://localhost:9000 and login. If all has been wired up well, then login will succeed", having never told the reader to enable OIDC or point QuestDB at the provider. It worked as a narrative because the reference page's endpoint defaults are Ping-shaped and the page also claimed `acl.oidc.groups.claim` defaulted to `groups` - the exact claim name this walkthrough builds. The property has had no default since the OIDC client was introduced, so the implied recipe never started a server. Correcting the default made the omission explicit: the reference page now says the property is mandatory and the walkthrough still does not mention it. Give the section the same `server.conf` block the Entra ID section has, and note that the endpoint defaults cover PingFederate. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 1 + documentation/security/oidc.mdx | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index 40e51f97be..f9bee8cdfb 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -50,6 +50,7 @@ This page tracks significant updates to the QuestDB documentation. - [read_parquet](/docs/query/functions/parquet/#designated-timestamp) - Documented nominating a designated timestamp on a Parquet file with `TIMESTAMP()` (applied directly, on a sub-query, or on a CTE), and importing a file into a table with `INSERT INTO ... SELECT` or `CREATE TABLE AS` - Documented the OIDC configuration options that were missing from the [OIDC page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.device.authorization.endpoint`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, and `acl.oidc.string.pool.capacity`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default - [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers +- [PingFederate SSO](/docs/security/oidc/#pingfederate) - Added the missing QuestDB `server.conf` block to the walkthrough, which relied on defaults for settings that have none - [OpenID Connect (OIDC)](/docs/security/oidc/#device-code-flow) - Documented the Device Code Flow for clients without a browser of their own, how QuestDB advertises the Device Authorization Endpoint to them, and why it is preferable to the Resource Owner Password Credentials flow ### Updated diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 7c5a58248a..3f121bce06 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -1101,6 +1101,37 @@ to the existing PCV. Then select the `username` attribute of the PCV as `USER_KEY`. +#### QuestDB configuration + +The below should be set in QuestDB's `server.conf`: + +```shell +# enable OIDC +acl.oidc.enabled=true + +# hostname of the PingFederate server +acl.oidc.host=pingfederate.host + +# the client id picked when setting up the client above +acl.oidc.client.id=questdb + +# the claim which contains the user's group memberships +acl.oidc.groups.claim=groups + +# enable ROPC flow +# optional, required only if ROPC is enabled in PingFederate +acl.oidc.ropc.flow.enabled=true +``` + +The endpoint defaults listed under +[Endpoints](/docs/configuration/oidc/#endpoints) already match the PingFederate +paths, so they do not have to be set. + +Alternatively, set `acl.oidc.configuration.url` instead of `acl.oidc.host` and +let QuestDB discover the endpoints from PingFederate. The two are mutually +exclusive, and the endpoint settings are not used when the configuration URL is +set. + #### Confirm QuestDB mappings and login QuestDB requires a mapping, as laid out in the From eec6f6d2a24c1f4dead2e652ce6e107cc3814b15 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 18:50:44 +0100 Subject: [PATCH 11/38] Use the repo's spelling convention in the state parameter entry "as a defence against" was the only instance of British "defence" in the docs, which otherwise favor American spelling. Reword rather than respell, which also tightens the sentence. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index c8ff2bb925..e2b73d505a 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -137,7 +137,7 @@ enabled, this flow must also be configured in the OIDC Provider. - **Default**: `false` - **Reloadable**: no -Requires the `state` parameter in the Authorization Code Flow, as a defence +Requires the `state` parameter in the Authorization Code Flow, which protects against CSRF attacks. QuestDB does not see the value itself; it publishes the setting to clients through the settings endpoint, the same way it publishes `acl.oidc.pkce.required`. From 6dab13728d6e538525d72cf0f5e707484dab0197 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 19:50:17 +0100 Subject: [PATCH 12/38] Name both QuestDB configuration sections after their provider Two headings called "QuestDB configuration" left the slug of the older Entra ID one to the newly added PingFederate section, silently sending every existing link to /docs/security/oidc/#questdb-configuration to the wrong provider. Pin the Entra ID slug so it keeps the anchor it has always had. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/security/oidc.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 3f121bce06..526eded08e 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -1101,7 +1101,7 @@ to the existing PCV. Then select the `username` attribute of the PCV as `USER_KEY`. -#### QuestDB configuration +#### QuestDB configuration for PingFederate The below should be set in QuestDB's `server.conf`: @@ -1326,7 +1326,7 @@ With this we have finished setting up the QuestDB client application in Entra ID, and now we can wire QuestDB and Entra ID together by adding OIDC configuration to QuestDB. -#### QuestDB configuration +#### QuestDB configuration for Entra ID {#questdb-configuration} The below should be set in QuestDB's `server.conf`: From e76c0f9f30c11076ab27326ae8781b25f9507c3f Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 19:50:17 +0100 Subject: [PATCH 13/38] Document the settings endpoint clients read the OIDC configuration from The settings endpoint was named three times without being described anywhere, so a client author had no path, no response shape and no field names to work with. The only entry which mentioned it, http.context.settings, presented it as a Web Console detail. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 1 + documentation/configuration/http-server.md | 6 ++- documentation/configuration/oidc.md | 22 +++++++--- documentation/security/oidc.mdx | 50 ++++++++++++++++++++++ 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index f9bee8cdfb..5ca80bffb1 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -31,6 +31,7 @@ This page tracks significant updates to the QuestDB documentation. - [ALTER TABLE SET FORMAT](/docs/query/sql/alter-table-set-format/) - New reference page for switching a table's partition storage format between `NATIVE` and `PARQUET` - [QWP configuration](/docs/configuration/qwp/) - Server-side settings for the QWP ingestion (`/write/v4`) and query (`/read/v1`) endpoints - [Check timestamp order](/docs/cookbook/sql/time-series/check-timestamp-order/) and [Check column sort order](/docs/cookbook/sql/advanced/check-column-sort-order/) - Two cookbook recipes that detect unsorted data with `lag()`: whether a table, CSV import or external Parquet file is ordered by its timestamp, and whether one column is sorted with respect to another +- [OIDC settings endpoint](/docs/security/oidc/#settings-endpoint) - How a client reads QuestDB's OIDC configuration from `/settings` to discover the authorization, token and device authorization endpoints, and whether PKCE and the `state` parameter are required ### Reference diff --git a/documentation/configuration/http-server.md b/documentation/configuration/http-server.md index 65b14ef09f..17bee5659b 100644 --- a/documentation/configuration/http-server.md +++ b/documentation/configuration/http-server.md @@ -493,8 +493,10 @@ Context path for the file import service. - **Default**: `/settings` - **Reloadable**: no -Context path for the service which provides server-side settings to the Web -Console. +Context path for the service which provides server-side settings to clients. The +[Web Console](/docs/getting-started/web-console/overview/) reads it, and so does +any OIDC client which discovers the provider's endpoints from the +[settings endpoint](/docs/security/oidc/#settings-endpoint). ### http.context.table.status diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index e2b73d505a..1bb5bf3b2c 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -138,9 +138,11 @@ enabled, this flow must also be configured in the OIDC Provider. - **Reloadable**: no Requires the `state` parameter in the Authorization Code Flow, which protects -against CSRF attacks. QuestDB does not see the value itself; it publishes the -setting to clients through the settings endpoint, the same way it publishes -`acl.oidc.pkce.required`. +against CSRF attacks. QuestDB does not see the value itself. It publishes the +setting to clients through the +[settings endpoint](/docs/security/oidc/#settings-endpoint), the same way it +publishes `acl.oidc.pkce.required`, and the client is what generates and checks +the value. Enable it if the OIDC Provider requires the `state` parameter, or to add CSRF protection on top of PKCE. The @@ -171,10 +173,16 @@ OIDC Device Authorization Endpoint, used by clients which authenticate with the [Device Code Flow](/docs/security/oidc/#device-code-flow). Unlike the other endpoints, it has no default value. -QuestDB does not run the Device Code Flow itself. It publishes this endpoint -to clients through the settings endpoint, so that a client knows where to -start the flow. A client which does not receive it has to be given the -endpoint directly, or resolve it from the OIDC Provider itself. +QuestDB does not run the Device Code Flow itself. It publishes this endpoint to +clients through the +[settings endpoint](/docs/security/oidc/#settings-endpoint), so that a client +knows where to start the flow. A client which does not receive it has to be +given the endpoint directly, or resolve it from the OIDC Provider itself. + +When `acl.oidc.configuration.url` is set, QuestDB takes this endpoint from the +provider's configuration document instead, if the provider advertises a +`device_authorization_endpoint`. It is the only endpoint discovery is allowed to +omit. ### acl.oidc.public.keys.endpoint diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 526eded08e..d0580ed376 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -318,6 +318,56 @@ and then sends the results back: } ``` +## Settings endpoint + +QuestDB publishes the parts of its OIDC configuration that a client needs in +order to start an authentication flow. Any client can read them from the +settings endpoint, which requires no authentication: + +```bash title="Settings request example" +curl https://questdb.host:9000/settings +``` + +The OIDC-related entries of the response look like this, alongside other server +settings: + +```json title="Settings response example" +{ + "config": { + "acl.oidc.enabled": true, + "acl.oidc.pkce.required": true, + "acl.oidc.state.required": false, + "acl.oidc.groups.encoded.in.token": false, + "acl.oidc.client.id": "questdb", + "acl.oidc.redirect.uri": "https://questdb.host:9000", + "acl.oidc.scope": "openid", + "acl.oidc.authorization.endpoint": "https://oidc.provider:443/as/authorization.oauth2", + "acl.oidc.token.endpoint": "https://oidc.provider:443/as/token.oauth2", + "acl.oidc.device.authorization.endpoint": "https://oidc.provider:443/as/device_authz.oauth2" + }, + "preferences.version": 0, + "preferences": {} +} +``` + +Each key is the name of the +[configuration option](/docs/configuration/oidc/) it carries. The +[Web Console](/docs/getting-started/web-console/overview/) reads them to build +its authorization request, and any other client can do the same instead of +hard coding the provider's details. + +The endpoints are absolute URLs, resolved either from `acl.oidc.host` and the +[endpoint settings](/docs/configuration/oidc/#endpoints), or from the provider's +configuration document when `acl.oidc.configuration.url` is set. +`acl.oidc.device.authorization.endpoint` is the only one which may be absent, +because it is the only one QuestDB can run without, and the only one with no +default: the value above is whatever the operator configured or discovery +returned. + +The path of the endpoint is set by +[`http.context.settings`](/docs/configuration/http-server/#httpcontextsettings), +and defaults to `/settings`. + ## Interactive clients Any interactive client - a UI, Jupyter notebook, CLI - can integrate with an From ad08e2ee4dbd1698b339c03d0b946637738c4dcc Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 19:50:17 +0100 Subject: [PATCH 14/38] Show how to run the Device Code Flow The section described the flow but gave nothing to run, and never said where the client gets the Token endpoint it polls. Add the three requests, state that there is no setting to switch the flow on, and note that the ID token is the one to take when the groups are encoded in it. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/security/oidc.mdx | 80 ++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index d0580ed376..e64573b039 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -600,22 +600,90 @@ device code, displays a short user code together with a verification URL, and polls the Token endpoint while the user completes the login in a browser on any device. -QuestDB does not run the flow itself. It publishes the OIDC Provider's Device -Authorization Endpoint to clients through its settings endpoint, so a client -knows where to start: +QuestDB does not run the flow itself, and there is no setting to switch it on. +The client talks to the OIDC Provider directly. QuestDB only advertises the +provider's Device Authorization Endpoint through its +[settings endpoint](#settings-endpoint), so that a client knows where to start: - When `acl.oidc.configuration.url` is set, QuestDB takes the endpoint from the provider's configuration document, provided the provider advertises a `device_authorization_endpoint`. - When the provider is configured by host, set [`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint) - explicitly. It is the only endpoint with no default value. + explicitly. It is the only endpoint with no default value, so take the path + from the OIDC Provider's documentation. If QuestDB does not advertise the endpoint, the client has to be given it directly, or resolve it from the OIDC Provider itself. -Once the client holds an access token, it presents it to QuestDB the same way -as any other token. +The OIDC Provider has to support the flow too, with the device code grant +enabled for the client. + +#### Run the flow + +First read the endpoints from QuestDB. The +[settings endpoint](#settings-endpoint) carries both the Device Authorization +Endpoint the flow starts at, and the Token endpoint it polls: + +```python title="Discover the endpoints" +import requests +import time + +settings = requests.get("https://questdb.host:9000/settings").json()["config"] +device_endpoint = settings["acl.oidc.device.authorization.endpoint"] +token_endpoint = settings["acl.oidc.token.endpoint"] +client_id = settings["acl.oidc.client.id"] +scope = settings["acl.oidc.scope"] +``` + +Then ask the OIDC Provider for a device code, and tell the user where to log in. +The fields of the response are the ones defined by +[RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628#section-3.2): + +```python title="Request a device code" +device = requests.post(device_endpoint, + data={"client_id": client_id, + "scope": scope}, + headers={"Content-Type": "application/x-www-form-urlencoded"}).json() + +print(f"Open {device['verification_uri']} and enter the code {device['user_code']}") +``` + +Finally, poll the Token endpoint until the user completes the login. The error +`authorization_pending` means the user has not finished yet, `slow_down` asks +for a longer interval, and any other error ends the flow: + +```python title="Poll for the access token" +interval = device.get("interval", 5) +while True: + time.sleep(interval) + response = requests.post(token_endpoint, + data={"grant_type": "urn:ietf:params:oauth:grant-type:device_code", + "device_code": device["device_code"], + "client_id": client_id}, + headers={"Content-Type": "application/x-www-form-urlencoded"}).json() + if "access_token" in response: + access_token = response["access_token"] + break + error = response.get("error") + if error == "slow_down": + interval += 5 + elif error != "authorization_pending": + raise RuntimeError(response) +``` + +The client now holds a token, and presents it to QuestDB the same way as any +other token, as shown in the [7th step](#7-database-access) of the Authorization +Code Flow. + +:::note + +Take the ID token from the response instead of the access token when +[`acl.oidc.groups.encoded.in.token`](/docs/configuration/oidc/#acloidcgroupsencodedintoken) +is set to `true`. QuestDB then reads the group memberships from the token +itself, and only the ID token carries them. + +::: ## Non-interactive clients From 8e96bb8fb3b208761d0d4a34abc90aa99e98896e Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 19:50:17 +0100 Subject: [PATCH 15/38] Correct what the public keys expiry controls Key rotation does not depend on the expiry, because a token signed with an uncached key triggers an immediate reload. The setting governs revocation. Both JWKS settings are also inert unless the groups are encoded in the token. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 1bb5bf3b2c..75f5a1dc12 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -193,6 +193,11 @@ JSON Web Key Set (JWKS) Endpoint. Provides the list of public keys used to decode and validate ID tokens issued by the OIDC Provider. The default value should work for the Ping Identity Platform. +The keys are only read when +[`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is `true`. +With the default user info flow QuestDB validates tokens by calling the user +info endpoint instead. + ### acl.oidc.token.endpoint - **Default**: `/as/token.oauth2` @@ -307,9 +312,16 @@ Expiry of the cached JSON Web Key Set (JWKS) in milliseconds. Also accepts a duration, such as `2m` or `120s`. QuestDB caches the public keys used to validate tokens issued by the OIDC -Provider, and reloads them from the public keys endpoint when the cache -expires. Lower it if the OIDC Provider rotates its signing keys frequently, at -the cost of more requests to the endpoint. +Provider, and reloads them from the public keys endpoint when the cache expires. + +Key rotation does not depend on this setting: a token signed with a key QuestDB +has not cached triggers an immediate reload. The expiry governs how long a key +the provider has already withdrawn stays usable, so lower it if signing keys are +revoked, at the cost of more requests to the endpoint. + +Only used when +[`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is `true`, +which is the only case in which QuestDB validates token signatures itself. ### acl.oidc.response.buffer.size From 58c7dfda304dc56806f653edd9d9e1a5597564e5 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 19:50:17 +0100 Subject: [PATCH 16/38] Name the response each protocol gives when the claims are missing Authentication does fail, but only the HTTP endpoint answers with 401. PGWire returns an authentication error instead. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/security/oidc.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index e64573b039..2183e8ae24 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -829,10 +829,12 @@ claim called `groups`. Its name is set with [`acl.oidc.groups.claim`](/docs/configuration/oidc/#acloidcgroupsclaim), which has no default and must be set whenever OIDC is enabled. -If the groups claim is missing or it is an empty list, authentication fails and -QuestDB replies with `401 Unauthorized`. The same happens when the claim named -by [`acl.oidc.sub.claim`](/docs/configuration/oidc/#acloidcsubclaim) is missing -or empty. +If the groups claim is missing or it is an empty list, authentication fails. The +same happens when the claim named by +[`acl.oidc.sub.claim`](/docs/configuration/oidc/#acloidcsubclaim) is missing or +empty. Over HTTP QuestDB replies with `401 Unauthorized`, while on the PGWire +endpoint the client receives an authentication error. Either way the reason is +only visible in the server log. The user has to have at least the `HTTP` permission to be able to successfully login via the [Web Console](/docs/getting-started/web-console/overview/). From fb1a3c22200729757bb77a83162740bf25ce2933 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 22:03:10 +0100 Subject: [PATCH 17/38] Remove the Device Code Flow documentation Server-side support for the Device Code Flow is not finished, the clients are still work in progress, and there are open PRs which will change the server behaviour further. Documenting it now would describe an interface which is going to move. Removes the `acl.oidc.device.authorization.endpoint` configuration entry, the Device Code Flow section of the OIDC guide with its worked example, the tip which pointed the CLI section at it, and the endpoint from the settings endpoint example. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 5 +- documentation/configuration/oidc.md | 20 ----- documentation/security/oidc.mdx | 109 +--------------------------- 3 files changed, 3 insertions(+), 131 deletions(-) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index 5ca80bffb1..6b8e897c43 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -31,7 +31,7 @@ This page tracks significant updates to the QuestDB documentation. - [ALTER TABLE SET FORMAT](/docs/query/sql/alter-table-set-format/) - New reference page for switching a table's partition storage format between `NATIVE` and `PARQUET` - [QWP configuration](/docs/configuration/qwp/) - Server-side settings for the QWP ingestion (`/write/v4`) and query (`/read/v1`) endpoints - [Check timestamp order](/docs/cookbook/sql/time-series/check-timestamp-order/) and [Check column sort order](/docs/cookbook/sql/advanced/check-column-sort-order/) - Two cookbook recipes that detect unsorted data with `lag()`: whether a table, CSV import or external Parquet file is ordered by its timestamp, and whether one column is sorted with respect to another -- [OIDC settings endpoint](/docs/security/oidc/#settings-endpoint) - How a client reads QuestDB's OIDC configuration from `/settings` to discover the authorization, token and device authorization endpoints, and whether PKCE and the `state` parameter are required +- [OIDC settings endpoint](/docs/security/oidc/#settings-endpoint) - How a client reads QuestDB's OIDC configuration from `/settings` to discover the authorization and token endpoints, and whether PKCE and the `state` parameter are required ### Reference @@ -49,10 +49,9 @@ This page tracks significant updates to the QuestDB documentation. - Added the [`cairo.sql.parquet.cache.memory.size`](/docs/configuration/cairo-engine/) configuration property (256 MB default), deprecating the slot-based `cairo.sql.parquet.frame.cache.capacity` - Documented [`cairo.root`](/docs/configuration/cairo-engine/) absolute-path behavior: the `conf`, `import`, `export`, `tmp`, and `.checkpoint` directories become siblings of the specified directory rather than children of the server root, so leave it at the default under Docker - [read_parquet](/docs/query/functions/parquet/#designated-timestamp) - Documented nominating a designated timestamp on a Parquet file with `TIMESTAMP()` (applied directly, on a sub-query, or on a CTE), and importing a file into a table with `INSERT INTO ... SELECT` or `CREATE TABLE AS` -- Documented the OIDC configuration options that were missing from the [OIDC page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.device.authorization.endpoint`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, and `acl.oidc.string.pool.capacity`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default +- Documented the OIDC configuration options that were missing from the [OIDC page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, and `acl.oidc.string.pool.capacity`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default - [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers - [PingFederate SSO](/docs/security/oidc/#pingfederate) - Added the missing QuestDB `server.conf` block to the walkthrough, which relied on defaults for settings that have none -- [OpenID Connect (OIDC)](/docs/security/oidc/#device-code-flow) - Documented the Device Code Flow for clients without a browser of their own, how QuestDB advertises the Device Authorization Endpoint to them, and why it is preferable to the Resource Owner Password Credentials flow ### Updated diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 75f5a1dc12..fe1829e518 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -164,26 +164,6 @@ provider's configuration document and the settings below are not used. OIDC Authorization Endpoint. The default value should work for the Ping Identity Platform. -### acl.oidc.device.authorization.endpoint - -- **Default**: none -- **Reloadable**: no - -OIDC Device Authorization Endpoint, used by clients which authenticate with -the [Device Code Flow](/docs/security/oidc/#device-code-flow). Unlike the -other endpoints, it has no default value. - -QuestDB does not run the Device Code Flow itself. It publishes this endpoint to -clients through the -[settings endpoint](/docs/security/oidc/#settings-endpoint), so that a client -knows where to start the flow. A client which does not receive it has to be -given the endpoint directly, or resolve it from the OIDC Provider itself. - -When `acl.oidc.configuration.url` is set, QuestDB takes this endpoint from the -provider's configuration document instead, if the provider advertises a -`device_authorization_endpoint`. It is the only endpoint discovery is allowed to -omit. - ### acl.oidc.public.keys.endpoint - **Default**: `/pf/JWKS` diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 2183e8ae24..1b4db78936 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -342,8 +342,7 @@ settings: "acl.oidc.redirect.uri": "https://questdb.host:9000", "acl.oidc.scope": "openid", "acl.oidc.authorization.endpoint": "https://oidc.provider:443/as/authorization.oauth2", - "acl.oidc.token.endpoint": "https://oidc.provider:443/as/token.oauth2", - "acl.oidc.device.authorization.endpoint": "https://oidc.provider:443/as/device_authz.oauth2" + "acl.oidc.token.endpoint": "https://oidc.provider:443/as/token.oauth2" }, "preferences.version": 0, "preferences": {} @@ -359,10 +358,6 @@ hard coding the provider's details. The endpoints are absolute URLs, resolved either from `acl.oidc.host` and the [endpoint settings](/docs/configuration/oidc/#endpoints), or from the provider's configuration document when `acl.oidc.configuration.url` is set. -`acl.oidc.device.authorization.endpoint` is the only one which may be absent, -because it is the only one QuestDB can run without, and the only one with no -default: the value above is whatever the operator configured or discovery -returned. The path of the endpoint is set by [`http.context.settings`](/docs/configuration/http-server/#httpcontextsettings), @@ -583,108 +578,6 @@ testldap=> testldap=> ``` -:::tip - -The Resource Owner Password Credentials flow requires the client to handle the -user's password. A client which is able to display a URL and poll for the -result should use the [Device Code Flow](#device-code-flow) instead. - -::: - -### Device Code Flow - -The [Device Code Flow](https://datatracker.ietf.org/doc/html/rfc8628) is the -standard way for a client with no browser of its own to obtain tokens without -ever handling the user's password. The client asks the OIDC Provider for a -device code, displays a short user code together with a verification URL, and -polls the Token endpoint while the user completes the login in a browser on any -device. - -QuestDB does not run the flow itself, and there is no setting to switch it on. -The client talks to the OIDC Provider directly. QuestDB only advertises the -provider's Device Authorization Endpoint through its -[settings endpoint](#settings-endpoint), so that a client knows where to start: - -- When `acl.oidc.configuration.url` is set, QuestDB takes the endpoint from the - provider's configuration document, provided the provider advertises a - `device_authorization_endpoint`. -- When the provider is configured by host, set - [`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint) - explicitly. It is the only endpoint with no default value, so take the path - from the OIDC Provider's documentation. - -If QuestDB does not advertise the endpoint, the client has to be given it -directly, or resolve it from the OIDC Provider itself. - -The OIDC Provider has to support the flow too, with the device code grant -enabled for the client. - -#### Run the flow - -First read the endpoints from QuestDB. The -[settings endpoint](#settings-endpoint) carries both the Device Authorization -Endpoint the flow starts at, and the Token endpoint it polls: - -```python title="Discover the endpoints" -import requests -import time - -settings = requests.get("https://questdb.host:9000/settings").json()["config"] -device_endpoint = settings["acl.oidc.device.authorization.endpoint"] -token_endpoint = settings["acl.oidc.token.endpoint"] -client_id = settings["acl.oidc.client.id"] -scope = settings["acl.oidc.scope"] -``` - -Then ask the OIDC Provider for a device code, and tell the user where to log in. -The fields of the response are the ones defined by -[RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628#section-3.2): - -```python title="Request a device code" -device = requests.post(device_endpoint, - data={"client_id": client_id, - "scope": scope}, - headers={"Content-Type": "application/x-www-form-urlencoded"}).json() - -print(f"Open {device['verification_uri']} and enter the code {device['user_code']}") -``` - -Finally, poll the Token endpoint until the user completes the login. The error -`authorization_pending` means the user has not finished yet, `slow_down` asks -for a longer interval, and any other error ends the flow: - -```python title="Poll for the access token" -interval = device.get("interval", 5) -while True: - time.sleep(interval) - response = requests.post(token_endpoint, - data={"grant_type": "urn:ietf:params:oauth:grant-type:device_code", - "device_code": device["device_code"], - "client_id": client_id}, - headers={"Content-Type": "application/x-www-form-urlencoded"}).json() - if "access_token" in response: - access_token = response["access_token"] - break - error = response.get("error") - if error == "slow_down": - interval += 5 - elif error != "authorization_pending": - raise RuntimeError(response) -``` - -The client now holds a token, and presents it to QuestDB the same way as any -other token, as shown in the [7th step](#7-database-access) of the Authorization -Code Flow. - -:::note - -Take the ID token from the response instead of the access token when -[`acl.oidc.groups.encoded.in.token`](/docs/configuration/oidc/#acloidcgroupsencodedintoken) -is set to `true`. QuestDB then reads the group memberships from the token -itself, and only the ID token carries them. - -::: - ## Non-interactive clients Non-interactive clients are usually jobs or standalone applications, such as a From 8733605be664aa4a5fe7ce7a8ee563785be67072 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 22:03:20 +0100 Subject: [PATCH 18/38] Correct when QuestDB reads the OIDC public keys The page said the keys are only read when the group memberships are encoded in the token. QuestDB builds the public keys store whenever OIDC is enabled, and the store downloads the keys from its constructor, so the endpoint is contacted at startup in the user info flow too. Only the use of the keys is conditional: token signatures are verified in the encoded-in-token flow. Names what the gate actually controls, and records that a failed download is logged rather than fatal, so an operator seeing that error in the log knows it is not a startup failure. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index fe1829e518..79e04f6f5d 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -173,11 +173,15 @@ JSON Web Key Set (JWKS) Endpoint. Provides the list of public keys used to decode and validate ID tokens issued by the OIDC Provider. The default value should work for the Ping Identity Platform. -The keys are only read when +The keys are only used to validate tokens when [`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is `true`. With the default user info flow QuestDB validates tokens by calling the user info endpoint instead. +QuestDB downloads the keys from this endpoint at startup either way, so that the +cache is never empty. A failure to download them is logged, and does not stop +the server. + ### acl.oidc.token.endpoint - **Default**: `/as/token.oauth2` From d827870b14364d69911a452f6a7f16c04e65b698 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 22:03:28 +0100 Subject: [PATCH 19/38] Say what disabling the user info cache does in each flow Setting the TTL to zero was documented as validating every request against the OIDC Provider. That holds only in the user info flow. When the group memberships are encoded in the token, revalidation verifies the token signature locally and never calls the provider, so disabling the cache buys no round trip and gives no revocation. The distinction matters because the Entra ID walkthrough turns that setting on, and an operator zeroing the TTL there would expect revocation. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 79e04f6f5d..472c58847b 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -284,8 +284,10 @@ User info cache entry TTL in milliseconds, as a plain integer only. QuestDB caches user info responses for each valid access token. This setting controls how often the access token is validated and user info refreshed. -Set it to `0` to disable the cache, so that every request is validated against -the OIDC Provider. +Set it to `0` to disable the cache, so that every request is revalidated. In the +default user info flow that means a call to the OIDC Provider on every request. +When [`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is +`true` the token is revalidated locally, and the provider is not contacted. ### acl.oidc.public.keys.expiry From 952e8d5d28112e44176db4cf9b82a117f7529549 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 22:03:35 +0100 Subject: [PATCH 20/38] Describe the whole OIDC guide, not just Web Console SSO The description ended at "QuestDB Enterprise Web Console", while the page also covers PGWire token authentication, group mapping, and the settings endpoint a client reads its OIDC configuration from. The build copies title and description verbatim into llms.txt, so that sentence is the whole of what a retrieval index sees for this page, and it pointed away from most of the content. Avoids a colon in the value, which breaks the YAML front matter. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/security/oidc.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 1b4db78936..b074d2ad04 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -1,6 +1,6 @@ --- title: OpenID Connect (OIDC) Integration -description: Configure OpenID Connect (OIDC) integration with external Identity Providers for SSO authentication in QuestDB Enterprise Web Console. +description: Integrate QuestDB Enterprise with an external OIDC Identity Provider for Web Console SSO, PGWire token authentication, group mapping, and the settings endpoint. --- import Screenshot from "@theme/Screenshot"; From 8792eac4997bc74ea3094f5d5ebb31e4fdf65741 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 22:40:19 +0100 Subject: [PATCH 21/38] Say what the OIDC settings actually require and enforce Add a Minimum configuration section carrying the four settings a working setup needs, and all eight conditions which make the server refuse to start. acl.oidc.enabled listed four of them and read as complete, so it now points at the section instead. The section also names acl.enabled, which OIDC needs and which neither page mentioned. Correct four entries which claimed more than the server does: - acl.oidc.pkce.required read as an enforcement switch. The server only publishes it, the same as acl.oidc.state.required, and enforces neither. Both entries now say so through a shared section intro, which also lets the state entry drop the mechanism it repeated. - acl.oidc.tls.keystore.password said the pair fails startup. The check runs only when OIDC is enabled. - acl.oidc.sub.claim read as cosmetic. An empty sub claim fails authentication, the same way a missing groups claim does. - acl.oidc.response.buffer.size advised raising it for large user info responses. The size which rejects a response is the request buffer, not this one, so the advice does not hold. It now states the accepted format, which takes K and M but no G, and what a response that does not fit looks like. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 81 +++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 472c58847b..94401110dd 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -16,6 +16,40 @@ Provider (IdP). For detailed information about OIDC, see the [OpenID Connect (OIDC) integration guide](/docs/security/oidc). +## Minimum configuration + +OIDC requires [`acl.enabled`](/docs/configuration/iam/#aclenabled) to be `true`, +which is the default. With access control disabled the OIDC settings are +ignored, and no OIDC authentication takes place. + +A working setup against a Ping Identity provider needs four settings. Every +other setting has a usable default: + +```shell +acl.oidc.enabled=true +acl.oidc.host=oidc.provider +acl.oidc.client.id=questdb +acl.oidc.groups.claim=groups +``` + +QuestDB refuses to start when the OIDC configuration is inconsistent. With +`acl.oidc.enabled=true`: + +- [`acl.oidc.client.id`](#acloidcclientid) and + [`acl.oidc.groups.claim`](#acloidcgroupsclaim) must be set. +- Exactly one of [`acl.oidc.host`](#acloidchost) and + [`acl.oidc.configuration.url`](#acloidcconfigurationurl) must be set. +- [`acl.basic.auth.realm.enabled`](/docs/configuration/iam/#aclbasicauthrealmenabled) + must be `false`. +- [`acl.oidc.tls.keystore.path`](#acloidctlskeystorepath) and + [`acl.oidc.tls.keystore.password`](#acloidctlskeystorepassword) must both be + set, or neither. +- [`acl.oidc.tls.enabled`](#acloidctlsenabled) must match the scheme of every + OIDC Provider URL. +- When [`acl.oidc.configuration.url`](#acloidcconfigurationurl) is set, the + document must be downloadable and parseable, and must name the authorization, + token, user info and JWKS endpoints. + ## General ### acl.oidc.audience @@ -57,7 +91,9 @@ Mutually exclusive with `acl.oidc.host`: setting both fails server startup. Enables or disables OIDC authentication. When enabled, `acl.oidc.client.id` and `acl.oidc.groups.claim` must also be set, along with either -`acl.oidc.host` or `acl.oidc.configuration.url`. +`acl.oidc.host` or `acl.oidc.configuration.url`. See +[Minimum configuration](#minimum-configuration) for the full set of startup +requirements. OIDC cannot be enabled together with [`acl.basic.auth.realm.enabled`](/docs/configuration/iam/#aclbasicauthrealmenabled). @@ -107,6 +143,12 @@ scope `openid` is mandatory and must always be included. ## Authentication flows +QuestDB publishes [`acl.oidc.pkce.required`](#acloidcpkcerequired) and +[`acl.oidc.state.required`](#acloidcstaterequired) to clients through the +[settings endpoint](/docs/security/oidc/#settings-endpoint), and enforces +neither. The client generates the code verifier and the `state` value, and +checks them. + ### acl.oidc.pg.token.as.password.enabled - **Default**: `false` @@ -121,8 +163,9 @@ contain the string `_sso`, or left empty if that is an option. - **Default**: `true` - **Reloadable**: no -Enables or disables PKCE for the Authorization Code Flow. This should always -be enabled in production. The Web Console is not fully secure without it. +Tells clients that PKCE is required for the Authorization Code Flow. This +should always be enabled in production. The Web Console is not fully secure +without it. ### acl.oidc.ropc.flow.enabled @@ -137,18 +180,14 @@ enabled, this flow must also be configured in the OIDC Provider. - **Default**: `false` - **Reloadable**: no -Requires the `state` parameter in the Authorization Code Flow, which protects -against CSRF attacks. QuestDB does not see the value itself. It publishes the -setting to clients through the -[settings endpoint](/docs/security/oidc/#settings-endpoint), the same way it -publishes `acl.oidc.pkce.required`, and the client is what generates and checks -the value. +Tells clients that the `state` parameter is required in the Authorization Code +Flow, which protects against CSRF attacks. Enable it if the OIDC Provider +requires the `state` parameter, or to add CSRF protection on top of PKCE. -Enable it if the OIDC Provider requires the `state` parameter, or to add CSRF -protection on top of PKCE. The -[Web Console](/docs/security/oidc/#1-secret-generation) generates the value, -sends it in the authorization request, and checks that the provider returns it -unchanged. +The [Web Console](/docs/getting-started/web-console/overview/) generates the +value, sends it in the authorization request, and checks that the provider +returns it unchanged. See +[Secret generation](/docs/security/oidc/#1-secret-generation). ## Endpoints @@ -224,7 +263,8 @@ A URL whose scheme does not match fails server startup. - **Reloadable**: no Keystore password. Must be set whenever `acl.oidc.tls.keystore.path` is set. -Setting either one without the other fails server startup. +When OIDC is enabled, setting either one without the other fails server +startup. ### acl.oidc.tls.keystore.path @@ -273,6 +313,10 @@ The name of the claim in the user information that contains the user's name. Could be a username, full name, or email. Displayed in the Web Console and logged for audit purposes. +The claim must be present and non-empty, otherwise authentication fails, the +same way it does when the groups claim is missing. See +[Mapping user permissions](/docs/security/oidc/#mapping-user-permissions). + ## Caching and buffers ### acl.oidc.cache.ttl @@ -315,8 +359,11 @@ which is the only case in which QuestDB validates token signatures itself. - **Reloadable**: no Size of the buffer used to receive HTTP responses from the OIDC Provider. -Increase it if the provider sends large responses, such as user info -containing a long list of group memberships. +Accepts a plain byte count, or a value with a `K` or `M` suffix, such as +`512K`. There is no `G` suffix. + +If a response from the OIDC Provider does not fit, authentication fails and the +reason is logged by the server. ### acl.oidc.string.pool.capacity From b875d2d7404d9de954a6b581ad449a3b1707c5fd Mon Sep 17 00:00:00 2001 From: glasstiger Date: Fri, 21 Aug 2026 22:40:27 +0100 Subject: [PATCH 22/38] Document what the settings endpoint guarantees to a client The section told any client to read the endpoints from /settings instead of hard coding them, without saying what the response guarantees. Add a table giving each key its type and when it is present, and note that a provider advertising a device authorization endpoint adds a tenth key. acl.oidc.client.id and acl.oidc.redirect.uri are published as null when unset, so the redirect URI now carries the fallback a client needs. Say to read acl.oidc.enabled first. The acl.oidc.* entries are published whether or not OIDC is enabled, and the endpoint URLs are then built from the defaults, so a client which skips the check sends users to a URL which looks real and is not. The same note names acl.enabled, which can leave acl.oidc.enabled reporting true while nothing authenticates. Correct http.context.settings, which said the Web Console reads it. The Web Console requests /settings and ignores the setting, so moving the path stops it from loading. Both pages now say to leave it alone. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/http-server.md | 9 ++++-- documentation/security/oidc.mdx | 35 ++++++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/documentation/configuration/http-server.md b/documentation/configuration/http-server.md index 17bee5659b..1470f4c820 100644 --- a/documentation/configuration/http-server.md +++ b/documentation/configuration/http-server.md @@ -493,11 +493,14 @@ Context path for the file import service. - **Default**: `/settings` - **Reloadable**: no -Context path for the service which provides server-side settings to clients. The -[Web Console](/docs/getting-started/web-console/overview/) reads it, and so does -any OIDC client which discovers the provider's endpoints from the +Context path for the service that serves server-side settings to clients, such +as an OIDC client discovering the provider's endpoints from the [settings endpoint](/docs/security/oidc/#settings-endpoint). +Leave this at the default. The +[Web Console](/docs/getting-started/web-console/overview/) requests `/settings` +and does not read this setting, so changing the path stops it from loading. + ### http.context.table.status - **Default**: `/chk` diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index b074d2ad04..b29fdc9829 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -355,13 +355,44 @@ Each key is the name of the its authorization request, and any other client can do the same instead of hard coding the provider's details. +| Key | Type | Present | +| --- | --- | --- | +| `acl.oidc.enabled` | boolean | always | +| `acl.oidc.pkce.required` | boolean | always | +| `acl.oidc.state.required` | boolean | always | +| `acl.oidc.groups.encoded.in.token` | boolean | always | +| `acl.oidc.client.id` | string or `null` | always | +| `acl.oidc.redirect.uri` | string or `null` | always | +| `acl.oidc.scope` | string | always | +| `acl.oidc.authorization.endpoint` | string | when the endpoint is resolved | +| `acl.oidc.token.endpoint` | string | when the endpoint is resolved | + The endpoints are absolute URLs, resolved either from `acl.oidc.host` and the [endpoint settings](/docs/configuration/oidc/#endpoints), or from the provider's -configuration document when `acl.oidc.configuration.url` is set. +configuration document when `acl.oidc.configuration.url` is set. A provider +which advertises a device authorization endpoint adds +`acl.oidc.device.authorization.endpoint` to the response as well. + +`acl.oidc.redirect.uri` is `null` unless +[`acl.oidc.redirect.uri`](/docs/configuration/oidc/#acloidcredirecturi) is set. +A client should then fall back to its own location, as the Web Console does. + +:::note + +Read `acl.oidc.enabled` before any of the other keys. The `acl.oidc.*` entries +are published whether or not OIDC is enabled, and with OIDC disabled the +endpoint URLs are built from the defaults rather than from a real provider. + +OIDC also requires [`acl.enabled`](/docs/configuration/iam/#aclenabled) to be +`true`. When access control is disabled, `acl.oidc.enabled` still reports the +configured value while no OIDC authentication takes place. + +::: The path of the endpoint is set by [`http.context.settings`](/docs/configuration/http-server/#httpcontextsettings), -and defaults to `/settings`. +and defaults to `/settings`. Leave it at the default: the Web Console requests +`/settings` and does not read this setting. ## Interactive clients From afb0e2a17e16ba87b26370e19f3f74a10ad721ab Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sat, 22 Aug 2026 00:18:57 +0100 Subject: [PATCH 23/38] Correct what overriding the settings context path does Both pages said to leave `http.context.settings` alone because changing it stops the Web Console from loading. It does not. After reading the override, PropServerConfiguration re-adds the default `http.context.web.console` + `/settings` unconditionally, under a comment saying it exists so that customization does not break the Web Console, and HttpServer binds the settings processor to every path in the set. Overriding the setting adds a path; the default keeps serving and the console keeps working. The `http.context.web.console` entry twenty-five lines below already documented this correctly, so the page contradicted itself. Keep the half that is true - the Web Console does request `/settings` and does not read the setting - and note that the property takes a comma-separated list, which neither page mentioned. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/http-server.md | 7 ++++--- documentation/security/oidc.mdx | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/documentation/configuration/http-server.md b/documentation/configuration/http-server.md index 1470f4c820..be97adbf46 100644 --- a/documentation/configuration/http-server.md +++ b/documentation/configuration/http-server.md @@ -497,9 +497,10 @@ Context path for the service that serves server-side settings to clients, such as an OIDC client discovering the provider's endpoints from the [settings endpoint](/docs/security/oidc/#settings-endpoint). -Leave this at the default. The -[Web Console](/docs/getting-started/web-console/overview/) requests `/settings` -and does not read this setting, so changing the path stops it from loading. +Accepts a comma-separated list of paths. Setting it adds paths rather than +moving the service: QuestDB keeps serving the default path as well, so the +[Web Console](/docs/getting-started/web-console/overview/), which requests +`/settings` and does not read this setting, keeps working. ### http.context.table.status diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index b29fdc9829..f5e2c29abd 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -391,8 +391,8 @@ configured value while no OIDC authentication takes place. The path of the endpoint is set by [`http.context.settings`](/docs/configuration/http-server/#httpcontextsettings), -and defaults to `/settings`. Leave it at the default: the Web Console requests -`/settings` and does not read this setting. +and defaults to `/settings`. Setting it adds paths rather than moving the +endpoint, so `/settings` keeps working whatever else is configured. ## Interactive clients From 7ac66b92a4351ae90635191f2af5c5adcb43388d Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sat, 22 Aug 2026 00:39:41 +0100 Subject: [PATCH 24/38] Stop overstating what the OIDC settings guarantee Four claims on the configuration page promised more than the server delivers. `acl.oidc.cache.ttl` said that with the groups encoded in the token the token is "revalidated locally, and the provider is not contacted". Both halves were too strong. The local check covers the signature and the audience; jwt.rs sets validate_exp to false, so the token's expiry is never tested. And the JWKS store still calls the provider when the cached keys expire or the key id is unknown. `acl.oidc.sub.claim` said a missing claim fails authentication "the same way it does when the groups claim is missing" - a rule this page did not contain. `acl.oidc.groups.claim` documented only the startup requirement, never the runtime failure, and unlike its sibling carried no link to the guide. State the runtime failure on both, and let sub.claim point at groups.claim. `acl.oidc.response.buffer.size` claimed that a response which does not fit fails authentication with a logged reason. The setting sizes the HTTP client's response parser buffer; no code path ties an overflow to that outcome. Keep the generic behaviour, which does hold. The PKCE and state paragraph said the client checks both values. It generates both, but the provider checks the verifier at the token endpoint; the client checks only the state value it gets back. Also widen the page description. It stopped at "configuration settings" while the page covers startup rules, endpoints, TLS, claims, caching and buffers, and both OIDC pages share a title - in llms.txt the description is the only thing telling them apart. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/configuration/oidc.md | 35 ++++++++++++++++++----------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 94401110dd..0444d69d9e 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -1,6 +1,6 @@ --- title: OpenID Connect (OIDC) -description: Configuration settings for OpenID Connect integration in QuestDB Enterprise. +description: "QuestDB Enterprise acl.oidc.* settings reference: minimum configuration and startup rules, endpoints, TLS, user and group claims, caching and buffers." --- :::note @@ -146,8 +146,9 @@ scope `openid` is mandatory and must always be included. QuestDB publishes [`acl.oidc.pkce.required`](#acloidcpkcerequired) and [`acl.oidc.state.required`](#acloidcstaterequired) to clients through the [settings endpoint](/docs/security/oidc/#settings-endpoint), and enforces -neither. The client generates the code verifier and the `state` value, and -checks them. +neither. The client generates the code verifier and the `state` value; the +provider checks the verifier, and the client checks the `state` value it gets +back. ### acl.oidc.pg.token.as.password.enabled @@ -295,6 +296,10 @@ which it connects. The name of the custom claim in the user information that contains the group memberships of the user. Required when OIDC is enabled. +If the claim is missing from the user information, or it is an empty list, +authentication fails. See +[Mapping user permissions](/docs/security/oidc/#mapping-user-permissions). + ### acl.oidc.groups.encoded.in.token - **Default**: `false` @@ -313,8 +318,9 @@ The name of the claim in the user information that contains the user's name. Could be a username, full name, or email. Displayed in the Web Console and logged for audit purposes. -The claim must be present and non-empty, otherwise authentication fails, the -same way it does when the groups claim is missing. See +If the claim is missing from the user information, or empty, authentication +fails. The same applies to the claim named by +[`acl.oidc.groups.claim`](#acloidcgroupsclaim). See [Mapping user permissions](/docs/security/oidc/#mapping-user-permissions). ## Caching and buffers @@ -328,10 +334,13 @@ User info cache entry TTL in milliseconds, as a plain integer only. QuestDB caches user info responses for each valid access token. This setting controls how often the access token is validated and user info refreshed. -Set it to `0` to disable the cache, so that every request is revalidated. In the -default user info flow that means a call to the OIDC Provider on every request. -When [`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is -`true` the token is revalidated locally, and the provider is not contacted. +Set it to `0` to disable the cache, so that every request is checked again. In +the default user info flow that means a call to the OIDC Provider on every +request. When +[`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is `true` +QuestDB checks the token's signature and audience locally instead, and contacts +the provider only when the public keys have to be reloaded. The local check +does not test the token's expiry. ### acl.oidc.public.keys.expiry @@ -358,11 +367,11 @@ which is the only case in which QuestDB validates token signatures itself. - **Default**: `1M` - **Reloadable**: no -Size of the buffer used to receive HTTP responses from the OIDC Provider. -Accepts a plain byte count, or a value with a `K` or `M` suffix, such as -`512K`. There is no `G` suffix. +Size of the buffer used to receive and parse HTTP responses from the OIDC +Provider. Accepts a plain byte count, or a value with a `K` or `M` suffix, such +as `512K`. There is no `G` suffix. -If a response from the OIDC Provider does not fit, authentication fails and the +When a request to the OIDC Provider fails, authentication fails with it and the reason is logged by the server. ### acl.oidc.string.pool.capacity From 76237322193bc42c23b2a829d575069d135d199f Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sat, 22 Aug 2026 00:39:49 +0100 Subject: [PATCH 25/38] Complete what the settings endpoint section tells a client The section enumerated the response but left out three things a client needs. `acl.oidc.device.authorization.endpoint` was described in prose below the table and missing from the table itself, and the prose attributed it solely to a provider advertising the endpoint. In host mode the property drives it - the project's own SettingsEndpointTest is host-based and its payload carries the key. Say configured or discovered, and give it a row. `acl.enabled` is published in the same response, immediately before the acl.oidc entries. The note warned that acl.oidc.enabled can read true while access control is off, and then withheld the key that settles it. Add it to the example and the table, and tell the client to read both. That QuestDB advertises the PKCE and state flags and enforces neither was stated only on the configuration page. A client author reading this section alone would conclude the server requires PKCE. Say it here too, in the same words. Also link the settings endpoint from the two sections whose readers are its audience. Browser-based clients builds an authorization request, and non-interactive clients posts to a token endpoint it currently hard codes. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/security/oidc.mdx | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index f5e2c29abd..2a17b0e9ee 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -334,6 +334,7 @@ settings: ```json title="Settings response example" { "config": { + "acl.enabled": true, "acl.oidc.enabled": true, "acl.oidc.pkce.required": true, "acl.oidc.state.required": false, @@ -357,6 +358,7 @@ hard coding the provider's details. | Key | Type | Present | | --- | --- | --- | +| `acl.enabled` | boolean | always | | `acl.oidc.enabled` | boolean | always | | `acl.oidc.pkce.required` | boolean | always | | `acl.oidc.state.required` | boolean | always | @@ -366,17 +368,23 @@ hard coding the provider's details. | `acl.oidc.scope` | string | always | | `acl.oidc.authorization.endpoint` | string | when the endpoint is resolved | | `acl.oidc.token.endpoint` | string | when the endpoint is resolved | +| `acl.oidc.device.authorization.endpoint` | string | when the endpoint is configured or discovered | The endpoints are absolute URLs, resolved either from `acl.oidc.host` and the [endpoint settings](/docs/configuration/oidc/#endpoints), or from the provider's -configuration document when `acl.oidc.configuration.url` is set. A provider -which advertises a device authorization endpoint adds -`acl.oidc.device.authorization.endpoint` to the response as well. +configuration document when `acl.oidc.configuration.url` is set. +`acl.oidc.device.authorization.endpoint` is present when a device authorization +endpoint has been configured, or when the provider advertises one. `acl.oidc.redirect.uri` is `null` unless [`acl.oidc.redirect.uri`](/docs/configuration/oidc/#acloidcredirecturi) is set. A client should then fall back to its own location, as the Web Console does. +QuestDB advertises `acl.oidc.pkce.required` and `acl.oidc.state.required`, and +enforces neither. The client generates the code verifier and the `state` value; +the provider checks the verifier, and the client checks the `state` value it +gets back. + :::note Read `acl.oidc.enabled` before any of the other keys. The `acl.oidc.*` entries @@ -384,8 +392,9 @@ are published whether or not OIDC is enabled, and with OIDC disabled the endpoint URLs are built from the defaults rather than from a real provider. OIDC also requires [`acl.enabled`](/docs/configuration/iam/#aclenabled) to be -`true`. When access control is disabled, `acl.oidc.enabled` still reports the -configured value while no OIDC authentication takes place. +`true`, which the same response carries. When access control is disabled, +`acl.oidc.enabled` still reports the configured value while no OIDC +authentication takes place, so read both keys. ::: @@ -419,6 +428,10 @@ possible flows to request an access token.: 2. [Implicit](https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth) flow +The client can read the authorization and token endpoints, the client id and +the scopes from the [settings endpoint](#settings-endpoint) instead of hard +coding them. + The Web Console implements the [Authorization Code Flow with PKCE](https://oauth.net/2/pkce), which is a special version of the Authorization Code flow designed for mobile apps and @@ -616,7 +629,9 @@ client for ingesting data. It is practical to manage their credentials via an OAuth2 provider too. As seen in the Jupyter notebook examples, the clients can request a token -themselves and then use it to authorise data ingestion: +themselves and then use it to authorise data ingestion. The token endpoint they +post to is published on the [settings endpoint](#settings-endpoint), so it does +not have to be hard coded: ```python import json From 32a17399f238b78b8e744992bd7b4b5522f5d13c Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sat, 22 Aug 2026 00:39:54 +0100 Subject: [PATCH 26/38] Point the client pages at the OIDC settings endpoint Every client page tells the reader to acquire a token out of band from their IdP, and links to the top of the OIDC guide, which drops them at the architecture diagram. None of them mentioned that QuestDB publishes the provider's authorization and token endpoints on its settings endpoint, so a client can discover them instead of hard coding them. These pages are the settings endpoint's audience, and it had no inbound link from any of them. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/connect/clients/c-and-cpp.md | 2 +- documentation/connect/clients/dotnet.md | 2 +- documentation/connect/clients/go.md | 3 +++ documentation/connect/clients/python.md | 2 +- documentation/connect/wire-protocols/qwp-ingress-websocket.md | 4 +++- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/documentation/connect/clients/c-and-cpp.md b/documentation/connect/clients/c-and-cpp.md index 24857c2724..3f091a5a9d 100644 --- a/documentation/connect/clients/c-and-cpp.md +++ b/documentation/connect/clients/c-and-cpp.md @@ -332,7 +332,7 @@ following are **not** supported: | Path | Status | Workaround | |---|---|---| -| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, pass it via `token=...`, and rebuild the pool when the token nears expiry. | +| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, discovering its authorization and token endpoints from QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint), pass it via `token=...`, and rebuild the pool when the token nears expiry. | | Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss`. See the connect-string reference's [TLS section](/docs/connect/clients/connect-string/#tls). | | Token rotation mid-session | Not supported. Credentials are presented once during the WebSocket upgrade and are not re-sent. | On token expiry, close the pool and build a fresh one with the new token. | diff --git a/documentation/connect/clients/dotnet.md b/documentation/connect/clients/dotnet.md index ca4dc261f4..d597deba9f 100644 --- a/documentation/connect/clients/dotnet.md +++ b/documentation/connect/clients/dotnet.md @@ -223,7 +223,7 @@ that purpose; left unset, it inherits `auth_timeout_ms`. | Path | Status | Workaround | |---|---|---| -| OIDC token acquisition or in-band refresh | Not supported by this client. It does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC — see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, pass it via `token=...` above, and rebuild the sender / query client when the token nears expiry. | +| OIDC token acquisition or in-band refresh | Not supported by this client. It does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC — see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, discovering its authorization and token endpoints from QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint), pass it via `token=...` above, and rebuild the sender / query client when the token nears expiry. | | Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss://`. See the connect-string reference for the canonical statement. | | Token rotation mid-session | Not supported. Credentials are presented once during the WebSocket upgrade and are not re-sent. | On token expiry, `await sender.DisposeAsync()` and build a fresh sender with the new token. The same applies to `QueryClient`. | diff --git a/documentation/connect/clients/go.md b/documentation/connect/clients/go.md index 78cb68217f..982b340406 100644 --- a/documentation/connect/clients/go.md +++ b/documentation/connect/clients/go.md @@ -362,6 +362,9 @@ the ingress and egress WebSocket upgrades. It is a **static credential**: the client sends exactly the string you pass and never refreshes or renews it. Acquire it out of band — QuestDB Enterprise issues bearer tokens through its [OpenID Connect flow](/docs/security/oidc/) — and manage its lifetime yourself. +QuestDB publishes the provider's authorization and token endpoints on its +[settings endpoint](/docs/security/oidc/#settings-endpoint), so a client can +discover them instead of hard coding them. When the token expires or is rotated, construct a new handle with the new token. An expired or rejected token surfaces as an authentication failure (see [Connection-level errors](#connection-level-errors)). It is mutually exclusive diff --git a/documentation/connect/clients/python.md b/documentation/connect/clients/python.md index 50de24dba0..0b275a0fcd 100644 --- a/documentation/connect/clients/python.md +++ b/documentation/connect/clients/python.md @@ -171,7 +171,7 @@ following are **not** supported: | Path | Status | Workaround | | --- | --- | --- | -| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and cannot refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, pass it via `token=...`, and rebuild the handle when the token nears expiry. | +| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and cannot refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, discovering its authorization and token endpoints from QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint), pass it via `token=...`, and rebuild the handle when the token nears expiry. | | Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss`. | | Token rotation mid-session | Not supported. The handle keeps the credentials it was built with and presents them on every connection it opens — including reconnects and failover, so an expired token also breaks mid-session reconnection. | On token expiry, close the handle and build a fresh one with the new token. | diff --git a/documentation/connect/wire-protocols/qwp-ingress-websocket.md b/documentation/connect/wire-protocols/qwp-ingress-websocket.md index e27c7ea5eb..2711a7feb4 100644 --- a/documentation/connect/wire-protocols/qwp-ingress-websocket.md +++ b/documentation/connect/wire-protocols/qwp-ingress-websocket.md @@ -143,7 +143,9 @@ Supported methods: [HTTP basic authentication](/docs/connect/compatibility/rest-api/#http-basic-authentication). - **Token-based auth** (Enterprise only): see [Authentication via token in QuestDB Enterprise](/docs/connect/compatibility/rest-api/#authentication-via-token-in-questdb-enterprise). -- **OIDC** (Enterprise only): see [OpenID Connect](/docs/security/oidc/). +- **OIDC** (Enterprise only): see [OpenID Connect](/docs/security/oidc/). The + [settings endpoint](/docs/security/oidc/#settings-endpoint) publishes the + provider's authorization and token endpoints. A failed authentication results in a `401` or `403` HTTP response before the WebSocket connection is established. No QWP-level auth handshake exists. From 6703c21edd248a7ce4935d94c6893d3f917c3083 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sat, 22 Aug 2026 00:40:00 +0100 Subject: [PATCH 27/38] Move the OIDC guide entries out of the reference section The docs-changelog criteria put config properties and syntax additions to reference pages under Reference, and rewritten sections and new examples under Updated. A note added to a walkthrough step and a new server.conf example in the PingFederate walkthrough are both guide changes, not reference ones. The PingFederate entry also linked to the top of a walkthrough some 330 lines long, rather than the configuration block it describes. The entry for the four missing configuration options stays under Reference, where it belongs. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index 6b8e897c43..356e13a881 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -50,11 +50,11 @@ This page tracks significant updates to the QuestDB documentation. - Documented [`cairo.root`](/docs/configuration/cairo-engine/) absolute-path behavior: the `conf`, `import`, `export`, `tmp`, and `.checkpoint` directories become siblings of the specified directory rather than children of the server root, so leave it at the default under Docker - [read_parquet](/docs/query/functions/parquet/#designated-timestamp) - Documented nominating a designated timestamp on a Parquet file with `TIMESTAMP()` (applied directly, on a sub-query, or on a CTE), and importing a file into a table with `INSERT INTO ... SELECT` or `CREATE TABLE AS` - Documented the OIDC configuration options that were missing from the [OIDC page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, and `acl.oidc.string.pool.capacity`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default -- [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers -- [PingFederate SSO](/docs/security/oidc/#pingfederate) - Added the missing QuestDB `server.conf` block to the walkthrough, which relied on defaults for settings that have none ### Updated +- [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers +- [PingFederate SSO](/docs/security/oidc/#questdb-configuration-for-pingfederate) - Added the missing QuestDB `server.conf` block to the walkthrough, which relied on defaults for settings that have none - Client libraries rewritten for the QWP binary protocol, unifying ingestion and streaming SQL queries under one handle: [Java](/docs/connect/clients/java/), [Python](/docs/connect/clients/python/), [Go](/docs/connect/clients/go/), [C & C++](/docs/connect/clients/c-and-cpp/), [Rust](/docs/connect/clients/rust/), and [.NET](/docs/connect/clients/dotnet/) - [Web Console](/docs/getting-started/web-console/overview/) - Documented query sharing by link and tab import/export in the [code editor](/docs/getting-started/web-console/code-editor/), custom AI providers and per-provider permission levels in [QuestDB AI](/docs/getting-started/web-console/questdb-ai/), automatic column sizing in the [result grid](/docs/getting-started/web-console/result-grid/), and the storage policy section in [table details](/docs/getting-started/web-console/table-details/) - [AI coding agents](/docs/getting-started/ai-coding-agents/) - Repositioned around the agent skill and the Web Console MCP bridge together From 5e815f7d74078cc3632826be2b539e5ad2f6bc93 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sun, 23 Aug 2026 22:35:06 +0100 Subject: [PATCH 28/38] Tell clients which token to send, and correct three claims Review of the OIDC pages turned up one gap and three statements that do not hold. Each is verified against the Enterprise source. Which token to send. The settings endpoint publishes acl.oidc.groups.encoded.in.token to every client and never said what to do with it, yet it decides which token belongs in the Authorization header: the ID token when the groups are encoded in it, the access token otherwise. Sending the wrong one is a 401 with the reason only in the log. The Web Console already picks the token this way. Documented in a new section, in the option's own entry, and in the ingestion example. The step 7 aside claiming the token "is rather opaque and does not contain user details" is only true of the access token, so it is now scoped: the ID token is a JWT whose payload is readable base64. The settings path. Setting http.context.settings adds paths rather than moving the service, but the path that survives is the Web Console context path plus /settings, not a literal /settings. Both pages said /settings keeps working whatever else is configured, which stops being true as soon as http.context.web.console is changed, as the neighbouring entry on the same page already noted. The Web Console also requests a relative "settings" rather than an absolute path. The non-interactive example. The prose said the token endpoint does not have to be hard coded and the example below it hard coded the endpoint and the client id. It now reads them from the settings endpoint, which is the only worked demonstration of the feature on the page. acl.enabled. Disabling access control stops OIDC authentication, but it does not stop the OIDC settings from being validated: the startup rules still fire and the provider's configuration document is still downloaded, so a broken OIDC configuration still prevents the server from starting. Also documented acl.oidc.device.authorization.endpoint, which the settings table already named while the configuration page did not. QuestDB never calls it and does not implement the Device Authorization Flow, it resolves the endpoint and republishes it for clients which do, so the option is documented without documenting the flow. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 3 +- documentation/configuration/http-server.md | 11 +++- documentation/configuration/oidc.md | 28 +++++++++- documentation/security/oidc.mdx | 64 ++++++++++++++++------ 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index 356e13a881..66d2b25747 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -49,11 +49,12 @@ This page tracks significant updates to the QuestDB documentation. - Added the [`cairo.sql.parquet.cache.memory.size`](/docs/configuration/cairo-engine/) configuration property (256 MB default), deprecating the slot-based `cairo.sql.parquet.frame.cache.capacity` - Documented [`cairo.root`](/docs/configuration/cairo-engine/) absolute-path behavior: the `conf`, `import`, `export`, `tmp`, and `.checkpoint` directories become siblings of the specified directory rather than children of the server root, so leave it at the default under Docker - [read_parquet](/docs/query/functions/parquet/#designated-timestamp) - Documented nominating a designated timestamp on a Parquet file with `TIMESTAMP()` (applied directly, on a sub-query, or on a CTE), and importing a file into a table with `INSERT INTO ... SELECT` or `CREATE TABLE AS` -- Documented the OIDC configuration options that were missing from the [OIDC page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, and `acl.oidc.string.pool.capacity`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default +- Documented the OIDC configuration options that were missing from the [OIDC configuration page](/docs/configuration/oidc/): `acl.oidc.state.required`, `acl.oidc.public.keys.expiry`, `acl.oidc.response.buffer.size`, `acl.oidc.string.pool.capacity`, and `acl.oidc.device.authorization.endpoint`; also corrected `acl.oidc.pkce.enabled` to its real name `acl.oidc.pkce.required`, and documented `acl.oidc.groups.claim` as mandatory with no default ### Updated - [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers +- [Which token to send](/docs/security/oidc/#which-token-to-send) - Documented that `acl.oidc.groups.encoded.in.token` decides whether a client sends the access token or the ID token, and that sending the wrong one fails authentication - [PingFederate SSO](/docs/security/oidc/#questdb-configuration-for-pingfederate) - Added the missing QuestDB `server.conf` block to the walkthrough, which relied on defaults for settings that have none - Client libraries rewritten for the QWP binary protocol, unifying ingestion and streaming SQL queries under one handle: [Java](/docs/connect/clients/java/), [Python](/docs/connect/clients/python/), [Go](/docs/connect/clients/go/), [C & C++](/docs/connect/clients/c-and-cpp/), [Rust](/docs/connect/clients/rust/), and [.NET](/docs/connect/clients/dotnet/) - [Web Console](/docs/getting-started/web-console/overview/) - Documented query sharing by link and tab import/export in the [code editor](/docs/getting-started/web-console/code-editor/), custom AI providers and per-provider permission levels in [QuestDB AI](/docs/getting-started/web-console/questdb-ai/), automatic column sizing in the [result grid](/docs/getting-started/web-console/result-grid/), and the storage policy section in [table details](/docs/getting-started/web-console/table-details/) diff --git a/documentation/configuration/http-server.md b/documentation/configuration/http-server.md index be97adbf46..6a7f4dfc1a 100644 --- a/documentation/configuration/http-server.md +++ b/documentation/configuration/http-server.md @@ -499,8 +499,15 @@ as an OIDC client discovering the provider's endpoints from the Accepts a comma-separated list of paths. Setting it adds paths rather than moving the service: QuestDB keeps serving the default path as well, so the -[Web Console](/docs/getting-started/web-console/overview/), which requests -`/settings` and does not read this setting, keeps working. +[Web Console](/docs/getting-started/web-console/overview/), which does not read +this setting, keeps working. + +The default path follows +[`http.context.web.console`](#httpcontextwebconsole), so it is `/settings` only +while that setting is at its default. Setting +`http.context.web.console=/console` makes the preserved path +`/console/settings`, and bare `/settings` is then served only if it is listed +here explicitly. ### http.context.table.status diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 0444d69d9e..bdc7fc943d 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -19,8 +19,12 @@ For detailed information about OIDC, see the ## Minimum configuration OIDC requires [`acl.enabled`](/docs/configuration/iam/#aclenabled) to be `true`, -which is the default. With access control disabled the OIDC settings are -ignored, and no OIDC authentication takes place. +which is the default. With access control disabled no OIDC authentication takes +place, but the OIDC settings are still validated at startup: with +`acl.oidc.enabled=true` the server enforces every rule below and downloads the +provider's configuration document, so an inconsistent OIDC configuration still +prevents it from starting. Set `acl.oidc.enabled=false` to take the settings out +of play entirely. A working setup against a Ping Identity provider needs four settings. Every other setting has a usable default: @@ -204,6 +208,20 @@ provider's configuration document and the settings below are not used. OIDC Authorization Endpoint. The default value should work for the Ping Identity Platform. +### acl.oidc.device.authorization.endpoint + +- **Default**: none +- **Reloadable**: no + +OIDC Device Authorization Endpoint. Unlike the other endpoint settings this one +has no default, and QuestDB never calls it. QuestDB resolves the endpoint and +publishes it on the +[settings endpoint](/docs/security/oidc/#settings-endpoint), for clients which +implement the Device Authorization Flow themselves. + +Left unset, and absent from the provider's configuration document, the endpoint +stays unresolved and the key is omitted from the settings response. + ### acl.oidc.public.keys.endpoint - **Default**: `/pf/JWKS` @@ -309,6 +327,12 @@ When `true`, QuestDB looks for group memberships in the ID token instead of calling the User Info endpoint. Set to `true` if the OIDC Provider encodes group memberships directly into the token. +This also changes which token the client has to send: the ID token when the +setting is `true`, the access token when it is `false`. QuestDB publishes the +setting on the +[settings endpoint](/docs/security/oidc/#settings-endpoint) so that clients can +[pick the right one](/docs/security/oidc/#which-token-to-send). + ### acl.oidc.sub.claim - **Default**: `sub` diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 2a17b0e9ee..2b36565a4c 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -240,10 +240,14 @@ The validity of the tokens are configurable inside the OIDC Provider. With the tokens, the Web Console can interact with the database. -The access token is in the header of every request sent to QuestDB. +A token is in the header of every request sent to QuestDB. Which of the two goes +there depends on where QuestDB reads the group memberships from, see +[Which token to send](#which-token-to-send). -> **Worried about exposing the token?** It is rather opaque and does not contain -> user details. +> **Worried about exposing the token?** The access token is rather opaque and +> does not contain user details. The ID token does: it is a JWT whose payload is +> base64 encoded, not encrypted, so treat it as you would the user's directory +> record. To carry out permission checks, the database has to know more about the user. @@ -373,8 +377,10 @@ hard coding the provider's details. The endpoints are absolute URLs, resolved either from `acl.oidc.host` and the [endpoint settings](/docs/configuration/oidc/#endpoints), or from the provider's configuration document when `acl.oidc.configuration.url` is set. -`acl.oidc.device.authorization.endpoint` is present when a device authorization -endpoint has been configured, or when the provider advertises one. +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint) +is present when a device authorization endpoint has been configured, or when the +provider advertises one. QuestDB publishes it without using it, for clients +which implement the Device Authorization Flow themselves. `acl.oidc.redirect.uri` is `null` unless [`acl.oidc.redirect.uri`](/docs/configuration/oidc/#acloidcredirecturi) is set. @@ -385,6 +391,24 @@ enforces neither. The client generates the code verifier and the `state` value; the provider checks the verifier, and the client checks the `state` value it gets back. +### Which token to send + +`acl.oidc.groups.encoded.in.token` tells the client which of the tokens returned +by the provider belongs in the `Authorization: Bearer` header: + +| Value | Token to send | How QuestDB validates it | +| --- | --- | --- | +| `false`, the default | the access token | by calling the user info endpoint | +| `true` | the ID token | locally, by checking its signature and audience | + +Sending the wrong one fails authentication with `401 Unauthorized` and the +reason in the server log. With +[`acl.oidc.groups.encoded.in.token`](/docs/configuration/oidc/#acloidcgroupsencodedintoken) +enabled the token has to be a JWT, because QuestDB reads the group memberships +straight out of its payload. The +[Web Console](/docs/getting-started/web-console/overview/) picks the token this +way, and so should any other client. + :::note Read `acl.oidc.enabled` before any of the other keys. The `acl.oidc.*` entries @@ -399,9 +423,13 @@ authentication takes place, so read both keys. ::: The path of the endpoint is set by -[`http.context.settings`](/docs/configuration/http-server/#httpcontextsettings), -and defaults to `/settings`. Setting it adds paths rather than moving the -endpoint, so `/settings` keeps working whatever else is configured. +[`http.context.settings`](/docs/configuration/http-server/#httpcontextsettings). +Setting it adds paths rather than moving the endpoint, so the default path keeps +working too. That default is `/settings` only while +[`http.context.web.console`](/docs/configuration/http-server/#httpcontextwebconsole) +is at its default, as the settings path follows the Web Console context path. +A client which cannot assume both are at their defaults should make the path +configurable. ## Interactive clients @@ -630,8 +658,10 @@ OAuth2 provider too. As seen in the Jupyter notebook examples, the clients can request a token themselves and then use it to authorise data ingestion. The token endpoint they -post to is published on the [settings endpoint](#settings-endpoint), so it does -not have to be hard coded: +post to, the client id and the scopes are all published on the +[settings endpoint](#settings-endpoint), so none of them have to be hard coded. +The same response also tells the client +[which token to send](#which-token-to-send): ```python import json @@ -645,20 +675,22 @@ load_dotenv() user = os.environ.get("username") pwd = os.environ.get("password") -token_endpoint = "https://oidc.provider:443/as/token.oauth2" -response = requests.post(token_endpoint, +settings = requests.get("http://localhost:9000/settings").json()["config"] + +response = requests.post(settings["acl.oidc.token.endpoint"], data={"grant_type": "password", - "client_id": "testclient", + "client_id": settings["acl.oidc.client.id"], "username": user, "password": pwd, - "scope": "openid"}, + "scope": settings["acl.oidc.scope"]}, headers={"Content-Type": "application/x-www-form-urlencoded"}) response_body = response.content.decode("utf-8") tokens = json.loads(response_body) -access_token = tokens["access_token"] +groups_in_token = settings["acl.oidc.groups.encoded.in.token"] +token = tokens["id_token"] if groups_in_token else tokens["access_token"] -conf = f"http::addr=localhost:9000;token={access_token};" +conf = f"http::addr=localhost:9000;token={token};" with Sender.from_conf(conf) as sender: df = pd.read_csv("data.csv") df["ts"] = pd.to_datetime(df["ts"]) From 390408ce9e9b5ecb0c766cc0da3424b962031f01 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sun, 23 Aug 2026 22:45:46 +0100 Subject: [PATCH 29/38] Say what QuestDB checks when it validates a token itself With acl.oidc.groups.encoded.in.token enabled QuestDB stops calling the provider and validates the ID token on its own. That it does not check the expiry was documented, but as the closing clause of acl.oidc.cache.ttl, a caching setting nobody opens unless they are tuning the cache. The consequence belongs where the mode is switched on. The option's own entry now carries the full picture: the signature, the audience and the presence of the claims are checked, exp, nbf and iss are not, and neither acl.oidc.cache.ttl nor a shorter token lifetime in the provider shortens how long an issued token is accepted. Once issued it stays good for as long as the signing key is in QuestDB's cache. The Entra ID walkthrough turns this mode on, because Entra ID cannot serve the group memberships from its User Info endpoint, so it now warns about it as well. acl.oidc.cache.ttl keeps a short note, because someone lowering the TTL to tighten revocation needs to know it will not help here, and points at the option for the detail. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 1 + documentation/configuration/oidc.md | 35 ++++++++++++++++++++++++++--- documentation/security/oidc.mdx | 13 +++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index 66d2b25747..c8ccdce603 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -55,6 +55,7 @@ This page tracks significant updates to the QuestDB documentation. - [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers - [Which token to send](/docs/security/oidc/#which-token-to-send) - Documented that `acl.oidc.groups.encoded.in.token` decides whether a client sends the access token or the ID token, and that sending the wrong one fails authentication +- [`acl.oidc.groups.encoded.in.token`](/docs/configuration/oidc/#acloidcgroupsencodedintoken) - Documented what QuestDB validates when it checks a token itself: the signature, the audience and the presence of the claims, but not `exp`, `nbf` or `iss`, so an expired token is still accepted - [PingFederate SSO](/docs/security/oidc/#questdb-configuration-for-pingfederate) - Added the missing QuestDB `server.conf` block to the walkthrough, which relied on defaults for settings that have none - Client libraries rewritten for the QWP binary protocol, unifying ingestion and streaming SQL queries under one handle: [Java](/docs/connect/clients/java/), [Python](/docs/connect/clients/python/), [Go](/docs/connect/clients/go/), [C & C++](/docs/connect/clients/c-and-cpp/), [Rust](/docs/connect/clients/rust/), and [.NET](/docs/connect/clients/dotnet/) - [Web Console](/docs/getting-started/web-console/overview/) - Documented query sharing by link and tab import/export in the [code editor](/docs/getting-started/web-console/code-editor/), custom AI providers and per-provider permission levels in [QuestDB AI](/docs/getting-started/web-console/questdb-ai/), automatic column sizing in the [result grid](/docs/getting-started/web-console/result-grid/), and the storage policy section in [table details](/docs/getting-started/web-console/table-details/) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index bdc7fc943d..6bad18d102 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -333,6 +333,31 @@ setting on the [settings endpoint](/docs/security/oidc/#settings-endpoint) so that clients can [pick the right one](/docs/security/oidc/#which-token-to-send). +It changes how tokens are validated too. In the default user info flow QuestDB +hands the token to the OIDC Provider on every cache miss, so the provider +decides whether it is still valid. With this setting enabled QuestDB validates +the token itself and never asks the provider about it: + +| Checked | Not checked | +| --- | --- | +| the signature, against the public key named by the token's `kid` | `exp`, the expiry | +| `aud`, against [`acl.oidc.audience`](#acloidcaudience) | `nbf`, the not-before time | +| that `sub` and the group memberships are present | `iss`, the issuer | + +:::caution + +An expired token is therefore still accepted. Once issued, a token stays valid +for as long as the key that signed it is in QuestDB's cache, which is governed +by [`acl.oidc.public.keys.expiry`](#acloidcpublickeysexpiry) and by how long the +provider publishes the key. Neither +[`acl.oidc.cache.ttl`](#acloidccachettl) nor a shorter token lifetime in the +provider shortens it. + +Do not enable this setting where you rely on being able to revoke a token, or on +the provider's token lifetimes being enforced. + +::: + ### acl.oidc.sub.claim - **Default**: `sub` @@ -362,9 +387,13 @@ Set it to `0` to disable the cache, so that every request is checked again. In the default user info flow that means a call to the OIDC Provider on every request. When [`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is `true` -QuestDB checks the token's signature and audience locally instead, and contacts -the provider only when the public keys have to be reloaded. The local check -does not test the token's expiry. +QuestDB checks the token locally instead, and contacts the provider only when +the public keys have to be reloaded. + +That local check does not test the token's expiry, so shortening this TTL does +not shorten how long an issued token is accepted. See +[`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) for what is +and is not validated. ### acl.oidc.public.keys.expiry diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 2b36565a4c..cccb664148 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -1465,6 +1465,19 @@ title="Application overview" width={600} /> +:::caution + +This setup relies on +[`acl.oidc.groups.encoded.in.token`](/docs/configuration/oidc/#acloidcgroupsencodedintoken), +because Entra ID cannot serve the group memberships from its User Info +endpoint. QuestDB then validates the ID token itself and never asks Entra ID +about it, which means it does not check the token's expiry: an issued token is +accepted for as long as the key that signed it stays in QuestDB's cache. Read +that option before going to production, and note that clients must send the ID +token here, not the access token. + +::: + #### Map groups and grant permissions Now we can start QuestDB, and login with the built-in admin to create From 4ddda15294398ce8f4e2c7e787ceedf548d31e28 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Sun, 23 Aug 2026 22:50:57 +0100 Subject: [PATCH 30/38] Scope three OIDC options to what the server actually does The same class of imprecision the rest of this branch fixed, left behind in three entries which were not otherwise being touched. acl.oidc.audience reads as though QuestDB always checks the audience. It only does when acl.oidc.groups.encoded.in.token is true, the one case in which it validates tokens itself. In the user info flow the provider decides, and the audience is never looked at. This is the qualifier already carried by acl.oidc.public.keys.expiry and acl.oidc.public.keys.endpoint. acl.oidc.scope said the openid scope "is mandatory and must always be included", on a page which now lists what makes the server refuse to start. It is the provider that enforces it: QuestDB passes the value on without inspecting it, so omitting openid fails at the provider. acl.oidc.ropc.flow.enabled said only that it enables the flow, which undersells it. QuestDB runs the flow itself, exchanging HTTP basic and PGWire credentials for a token at the provider, which is what lets psql and similar clients log in with SSO credentials. Local users are matched first, so a local account shadows an Identity Provider one of the same name. The setting is also absent from the settings endpoint, so a client cannot discover whether the flow is available. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/changelog.mdx | 1 + documentation/configuration/oidc.md | 32 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index c8ccdce603..9421bec728 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -56,6 +56,7 @@ This page tracks significant updates to the QuestDB documentation. - [OpenID Connect (OIDC)](/docs/security/oidc/#1-secret-generation) - Noted that some Identity Providers require the `state` parameter in the authorization request, and that `acl.oidc.state.required` should be set to `true` for those providers - [Which token to send](/docs/security/oidc/#which-token-to-send) - Documented that `acl.oidc.groups.encoded.in.token` decides whether a client sends the access token or the ID token, and that sending the wrong one fails authentication - [`acl.oidc.groups.encoded.in.token`](/docs/configuration/oidc/#acloidcgroupsencodedintoken) - Documented what QuestDB validates when it checks a token itself: the signature, the audience and the presence of the claims, but not `exp`, `nbf` or `iss`, so an expired token is still accepted +- Scoped three [OIDC options](/docs/configuration/oidc/) to what the server actually does: `acl.oidc.audience` is only checked when the groups are encoded in the token, the mandatory `openid` in `acl.oidc.scope` is enforced by the provider rather than at startup, and `acl.oidc.ropc.flow.enabled` makes QuestDB itself exchange HTTP basic and PGWire credentials for a token at the provider - [PingFederate SSO](/docs/security/oidc/#questdb-configuration-for-pingfederate) - Added the missing QuestDB `server.conf` block to the walkthrough, which relied on defaults for settings that have none - Client libraries rewritten for the QWP binary protocol, unifying ingestion and streaming SQL queries under one handle: [Java](/docs/connect/clients/java/), [Python](/docs/connect/clients/python/), [Go](/docs/connect/clients/go/), [C & C++](/docs/connect/clients/c-and-cpp/), [Rust](/docs/connect/clients/rust/), and [.NET](/docs/connect/clients/dotnet/) - [Web Console](/docs/getting-started/web-console/overview/) - Documented query sharing by link and tab import/export in the [code editor](/docs/getting-started/web-console/code-editor/), custom AI providers and per-provider permission levels in [QuestDB AI](/docs/getting-started/web-console/questdb-ai/), automatic column sizing in the [result grid](/docs/getting-started/web-console/result-grid/), and the storage policy section in [table details](/docs/getting-started/web-console/table-details/) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 6bad18d102..0fb8daa107 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -64,6 +64,12 @@ QuestDB refuses to start when the OIDC configuration is inconsistent. With OAuth2 audience as set on the tokens issued by the OIDC Provider. Defaults to the client ID if not set. +Only used when +[`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is `true`, +which is the only case in which QuestDB validates tokens itself. In the default +user info flow the OIDC Provider decides whether the token is valid, and +QuestDB does not check the audience at all. + ### acl.oidc.client.id - **Default**: none @@ -143,7 +149,15 @@ location where it was loaded from (`window.location.href`). - **Reloadable**: no The OIDC server asks consent for the scopes listed in this property. The -scope `openid` is mandatory and must always be included. +scope `openid` is mandatory and must always be included. That is an OIDC +protocol requirement enforced by the provider, not a QuestDB startup check: +QuestDB passes the value on without inspecting it, so leaving `openid` out +fails at the provider rather than at startup. + +QuestDB uses the scopes in the requests it makes itself, in the +[ROPC flow](#acloidcropcflowenabled), and publishes them on the +[settings endpoint](/docs/security/oidc/#settings-endpoint) for clients which +run the flow themselves. ## Authentication flows @@ -180,6 +194,22 @@ without it. Enables or disables the Resource Owner Password Credentials flow. When enabled, this flow must also be configured in the OIDC Provider. +With it enabled QuestDB runs the flow itself: a username and password arriving +over HTTP basic authentication or PGWire that match no local user are sent on to +the OIDC Provider's token endpoint as a password grant, and the user is logged +in if the provider issues a token. This lets clients which cannot follow a +browser redirect, such as `psql`, authenticate with their SSO credentials. + +Local users are matched first, so a QuestDB user whose name also exists in the +Identity Provider is authenticated against its local password, without involving +the provider. + +Unlike [`acl.oidc.pkce.required`](#acloidcpkcerequired) and +[`acl.oidc.state.required`](#acloidcstaterequired), this setting is not +published on the +[settings endpoint](/docs/security/oidc/#settings-endpoint), so a client cannot +discover whether the flow is available. + ### acl.oidc.state.required - **Default**: `false` From 1b30683b0b90b0c078da21eb6d99e97a2dc020a0 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Mon, 24 Aug 2026 00:20:47 +0100 Subject: [PATCH 31/38] Stop the client pages naming the wrong token The three client pages which list OIDC token acquisition as unsupported tell the reader to acquire an access token out of band. That is the wrong token whenever acl.oidc.groups.encoded.in.token is set, which is what the Entra ID walkthrough requires, because Entra ID cannot serve the group memberships from its User Info endpoint. Sending the access token there fails with a 401 and the reason only in the server log. Pointing these pages at the settings endpoint gave them a link to the response which carries the answer, but not to the section which explains it, and no client page linked "Which token to send" at all. Each now links it next to the settings endpoint and asks for "a token" rather than "an access token". The Go page and the QWP ingress page had the same missing link without the wrong noun, so they get it too. Also fixes a referent: "discovering its authorization and token endpoints from QuestDB's settings endpoint" attached "its" to the IdP, while the endpoints are the ones QuestDB publishes. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/connect/clients/c-and-cpp.md | 2 +- documentation/connect/clients/dotnet.md | 2 +- documentation/connect/clients/go.md | 4 +++- documentation/connect/clients/python.md | 2 +- documentation/connect/wire-protocols/qwp-ingress-websocket.md | 3 ++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/documentation/connect/clients/c-and-cpp.md b/documentation/connect/clients/c-and-cpp.md index 3f091a5a9d..5b65faf780 100644 --- a/documentation/connect/clients/c-and-cpp.md +++ b/documentation/connect/clients/c-and-cpp.md @@ -332,7 +332,7 @@ following are **not** supported: | Path | Status | Workaround | |---|---|---| -| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, discovering its authorization and token endpoints from QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint), pass it via `token=...`, and rebuild the pool when the token nears expiry. | +| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...`, and rebuild the pool when the token nears expiry. | | Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss`. See the connect-string reference's [TLS section](/docs/connect/clients/connect-string/#tls). | | Token rotation mid-session | Not supported. Credentials are presented once during the WebSocket upgrade and are not re-sent. | On token expiry, close the pool and build a fresh one with the new token. | diff --git a/documentation/connect/clients/dotnet.md b/documentation/connect/clients/dotnet.md index d597deba9f..24286e1fa1 100644 --- a/documentation/connect/clients/dotnet.md +++ b/documentation/connect/clients/dotnet.md @@ -223,7 +223,7 @@ that purpose; left unset, it inherits `auth_timeout_ms`. | Path | Status | Workaround | |---|---|---| -| OIDC token acquisition or in-band refresh | Not supported by this client. It does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC — see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, discovering its authorization and token endpoints from QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint), pass it via `token=...` above, and rebuild the sender / query client when the token nears expiry. | +| OIDC token acquisition or in-band refresh | Not supported by this client. It does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC — see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...` above, and rebuild the sender / query client when the token nears expiry. | | Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss://`. See the connect-string reference for the canonical statement. | | Token rotation mid-session | Not supported. Credentials are presented once during the WebSocket upgrade and are not re-sent. | On token expiry, `await sender.DisposeAsync()` and build a fresh sender with the new token. The same applies to `QueryClient`. | diff --git a/documentation/connect/clients/go.md b/documentation/connect/clients/go.md index 982b340406..135aa4afc4 100644 --- a/documentation/connect/clients/go.md +++ b/documentation/connect/clients/go.md @@ -364,7 +364,9 @@ Acquire it out of band — QuestDB Enterprise issues bearer tokens through its [OpenID Connect flow](/docs/security/oidc/) — and manage its lifetime yourself. QuestDB publishes the provider's authorization and token endpoints on its [settings endpoint](/docs/security/oidc/#settings-endpoint), so a client can -discover them instead of hard coding them. +discover them instead of hard coding them. The same response tells the client +[which token to send](/docs/security/oidc/#which-token-to-send): the access +token, or the ID token when QuestDB reads group memberships from the token. When the token expires or is rotated, construct a new handle with the new token. An expired or rejected token surfaces as an authentication failure (see [Connection-level errors](#connection-level-errors)). It is mutually exclusive diff --git a/documentation/connect/clients/python.md b/documentation/connect/clients/python.md index 0b275a0fcd..cfd3b1c404 100644 --- a/documentation/connect/clients/python.md +++ b/documentation/connect/clients/python.md @@ -171,7 +171,7 @@ following are **not** supported: | Path | Status | Workaround | | --- | --- | --- | -| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and cannot refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire an access token out-of-band from your IdP, discovering its authorization and token endpoints from QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint), pass it via `token=...`, and rebuild the handle when the token nears expiry. | +| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and cannot refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...`, and rebuild the handle when the token nears expiry. | | Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss`. | | Token rotation mid-session | Not supported. The handle keeps the credentials it was built with and presents them on every connection it opens — including reconnects and failover, so an expired token also breaks mid-session reconnection. | On token expiry, close the handle and build a fresh one with the new token. | diff --git a/documentation/connect/wire-protocols/qwp-ingress-websocket.md b/documentation/connect/wire-protocols/qwp-ingress-websocket.md index 2711a7feb4..0d64562109 100644 --- a/documentation/connect/wire-protocols/qwp-ingress-websocket.md +++ b/documentation/connect/wire-protocols/qwp-ingress-websocket.md @@ -145,7 +145,8 @@ Supported methods: [Authentication via token in QuestDB Enterprise](/docs/connect/compatibility/rest-api/#authentication-via-token-in-questdb-enterprise). - **OIDC** (Enterprise only): see [OpenID Connect](/docs/security/oidc/). The [settings endpoint](/docs/security/oidc/#settings-endpoint) publishes the - provider's authorization and token endpoints. + provider's authorization and token endpoints, and tells the client + [which token to send](/docs/security/oidc/#which-token-to-send). A failed authentication results in a `401` or `403` HTTP response before the WebSocket connection is established. No QWP-level auth handshake exists. From e7bd1848c7b890f716edd91a5489049e1d191176 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Mon, 24 Aug 2026 00:20:56 +0100 Subject: [PATCH 32/38] Give the ingestion example its own client id, and guard its lookups Reading the client id and the scopes from the settings endpoint went a step too far. acl.oidc.client.id and acl.oidc.scope describe QuestDB's own registration with the provider, so the example had every ingestion job present the Web Console's OAuth2 client, against the page's own advice that each application which integrates via OIDC should be given a different Client Id. The borrowed scope is whatever QuestDB asks consent for, which under the Entra ID walkthrough is "openid profile offline_access", so the job requested a refresh token it never uses. The token endpoint stays discovered, being a server fact. Two lookups could also fail on a bare KeyError. acl.oidc.token.endpoint is absent when acl.oidc.configuration.url is set while OIDC is disabled, which is what the presence table means by "when the endpoint is resolved" and what the note above the example means by reading acl.oidc.enabled first. The example did neither, and now gates on that flag. The password grant returns an id_token only if the provider issues one for it and openid is among the requested scopes, which QuestDB does not enforce, so the example says so and fails with a diagnostic rather than a KeyError. Also replaces the manual json.loads of response.content with response.json(), and adds raise_for_status so a refused grant does not surface three lines later as a missing token. Co-Authored-By: Claude Opus 5 (1M context) --- documentation/security/oidc.mdx | 49 +++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index cccb664148..e89ce6af7f 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -657,14 +657,19 @@ client for ingesting data. It is practical to manage their credentials via an OAuth2 provider too. As seen in the Jupyter notebook examples, the clients can request a token -themselves and then use it to authorise data ingestion. The token endpoint they -post to, the client id and the scopes are all published on the -[settings endpoint](#settings-endpoint), so none of them have to be hard coded. -The same response also tells the client -[which token to send](#which-token-to-send): +themselves and then use it to authorise data ingestion. QuestDB publishes the +provider's token endpoint on the [settings endpoint](#settings-endpoint), so it +does not have to be hard coded, and the same response tells the client +[which token to send](#which-token-to-send). + +The client id and the scopes stay the client's own. `acl.oidc.client.id` and +`acl.oidc.scope` describe QuestDB's registration with the provider, not the +job's. As noted in [Architecture overview](#architecture-overview), each +application which integrates via OIDC should be given a different Client Id, so +that the two can be told apart in the provider's audit log and given different +policies. ```python -import json import os import requests import pandas as pd @@ -675,20 +680,30 @@ load_dotenv() user = os.environ.get("username") pwd = os.environ.get("password") +# this job's own registration in the OIDC Provider, not QuestDB's; +# the openid scope is what makes the provider issue an ID token +client_id = os.environ.get("client_id") +scope = "openid" + settings = requests.get("http://localhost:9000/settings").json()["config"] +if not settings.get("acl.oidc.enabled"): + raise SystemExit("OIDC is not enabled on this QuestDB instance") response = requests.post(settings["acl.oidc.token.endpoint"], data={"grant_type": "password", - "client_id": settings["acl.oidc.client.id"], + "client_id": client_id, "username": user, "password": pwd, - "scope": settings["acl.oidc.scope"]}, + "scope": scope}, headers={"Content-Type": "application/x-www-form-urlencoded"}) +response.raise_for_status() +tokens = response.json() -response_body = response.content.decode("utf-8") -tokens = json.loads(response_body) -groups_in_token = settings["acl.oidc.groups.encoded.in.token"] -token = tokens["id_token"] if groups_in_token else tokens["access_token"] +# QuestDB reads the group memberships from the ID token when this is enabled +token_key = "id_token" if settings["acl.oidc.groups.encoded.in.token"] else "access_token" +if token_key not in tokens: + raise SystemExit(f"the provider did not issue an {token_key} for the password grant") +token = tokens[token_key] conf = f"http::addr=localhost:9000;token={token};" with Sender.from_conf(conf) as sender: @@ -697,6 +712,16 @@ with Sender.from_conf(conf) as sender: sender.dataframe(df, table_name="foo", at="ts") ``` +:::note + +The Resource Owner Password Credentials flow returns an ID token only if the +provider issues one for the password grant, and only when `openid` is among the +requested scopes. Check that the provider does before relying on +[`acl.oidc.groups.encoded.in.token`](/docs/configuration/oidc/#acloidcgroupsencodedintoken) +with a non-interactive client. + +::: + Alternatively, a user may rely on QuestDB to authenticate them via the OAuth2 provider when the Resource Owner Password Credentials flow is enabled on the server side: From 1b79708f6f9a66b476cc3c9757c6777fd70ddc71 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Wed, 2 Sep 2026 16:05:01 +0100 Subject: [PATCH 33/38] Document OIDC device flow authentication --- documentation/changelog.mdx | 3 +- documentation/configuration/oidc.md | 10 +- documentation/connect/clients/c-and-cpp.md | 111 +++++++- documentation/connect/clients/java.md | 40 +++ documentation/connect/clients/python.md | 54 +++- documentation/connect/clients/rust.md | 50 ++++ documentation/security/oidc.mdx | 298 ++++++++++++++++++++- 7 files changed, 533 insertions(+), 33 deletions(-) diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index 2e0d2b86b0..433d854b70 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -42,7 +42,8 @@ This page tracks significant updates to the QuestDB documentation. - [ALTER TABLE SET FORMAT](/docs/query/sql/alter-table-set-format/) - New reference page for switching a table's partition storage format between `NATIVE` and `PARQUET` - [QWP configuration](/docs/configuration/qwp/) - Server-side settings for the QWP ingestion (`/write/v4`) and query (`/read/v1`) endpoints - [Check timestamp order](/docs/cookbook/sql/time-series/check-timestamp-order/) and [Check column sort order](/docs/cookbook/sql/advanced/check-column-sort-order/) - Two cookbook recipes that detect unsorted data with `lag()`: whether a table, CSV import or external Parquet file is ordered by its timestamp, and whether one column is sorted with respect to another -- [OIDC settings endpoint](/docs/security/oidc/#settings-endpoint) - How a client reads QuestDB's OIDC configuration from `/settings` to discover the authorization and token endpoints, and whether PKCE and the `state` parameter are required +- [OIDC Device Authorization Flow](/docs/security/oidc/#device-authorization-flow) - Interactive OIDC sign-in for headless and remote applications, including configuration, token lifecycle, a protocol walkthrough, and examples for the Java, Python, Rust, C, and C++ clients +- [OIDC settings endpoint](/docs/security/oidc/#settings-endpoint) - How a client reads QuestDB's OIDC configuration from `/settings` to discover the authorization, token, and device authorization endpoints, and whether PKCE and the `state` parameter are required - [Kubernetes Operator](/docs/enterprise-kubernetes-operator/) - New manual for running QuestDB Enterprise clusters on Kubernetes, covering [installation](/docs/enterprise-kubernetes-operator/installation/), getting started on [AWS](/docs/enterprise-kubernetes-operator/getting-started/aws/) and [Azure](/docs/enterprise-kubernetes-operator/getting-started/azure/), [configuration](/docs/enterprise-kubernetes-operator/configuration/), [day-to-day operations](/docs/enterprise-kubernetes-operator/operations/operator/), [high availability](/docs/enterprise-kubernetes-operator/high-availability/), [known limitations](/docs/enterprise-kubernetes-operator/known-limitations/), and the full [API reference](/docs/enterprise-kubernetes-operator/reference/api/) - [ALTER TABLE SUSPEND WAL](/docs/query/sql/alter-table-suspend-wal/) - New reference page for deliberately stopping the WAL apply job, covering the quiescent-table use case that `REBASE WAL` requires, and the trap that writes to a suspended table succeed while staying invisible to queries until `RESUME WAL` diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 0fb8daa107..849dc66212 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -159,6 +159,11 @@ QuestDB uses the scopes in the requests it makes itself, in the [settings endpoint](/docs/security/oidc/#settings-endpoint) for clients which run the flow themselves. +For the [Device Authorization Flow](/docs/security/oidc/#device-authorization-flow), +add `offline_access` when the provider requires that scope before issuing a +refresh token. Without a refresh token, a client must ask the user to sign in +again after the current token expires. + ## Authentication flows QuestDB publishes [`acl.oidc.pkce.required`](#acloidcpkcerequired) and @@ -247,7 +252,10 @@ OIDC Device Authorization Endpoint. Unlike the other endpoint settings this one has no default, and QuestDB never calls it. QuestDB resolves the endpoint and publishes it on the [settings endpoint](/docs/security/oidc/#settings-endpoint), for clients which -implement the Device Authorization Flow themselves. +implement the +[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow) +themselves. The official Java, Python, Rust, C, and C++ clients can discover and +use the published endpoint. Left unset, and absent from the provider's configuration document, the endpoint stays unresolved and the key is omitted from the settings response. diff --git a/documentation/connect/clients/c-and-cpp.md b/documentation/connect/clients/c-and-cpp.md index 5b65faf780..f2830ece6d 100644 --- a/documentation/connect/clients/c-and-cpp.md +++ b/documentation/connect/clients/c-and-cpp.md @@ -325,16 +325,111 @@ Because the pool connects lazily, a bad credential surfaces as Handle it there, not at `connect` (see [Which errors mean what](#which-errors-mean-what)). -### Unsupported auth paths +### OIDC device flow (Enterprise) -The client supports only HTTP basic auth and static bearer-token auth. The -following are **not** supported: +The C and C++ APIs sign in an interactive user with the +[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow). +They discover the provider endpoints, client ID, scope, and the token QuestDB +expects from the server's public `/settings` endpoint. Attach the resulting auth +object to the pool so sender and reader connections share its rotating token: -| Path | Status | Workaround | -|---|---|---| -| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...`, and rebuild the pool when the token nears expiry. | -| Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss`. See the connect-string reference's [TLS section](/docs/connect/clients/connect-string/#tls). | -| Token rotation mid-session | Not supported. Credentials are presented once during the WebSocket upgrade and are not re-sent. | On token expiry, close the pool and build a fresh one with the new token. | + + + +```cpp +#include +#include +#include + +int main() { + auto auth = questdb::oidc::builder::from_questdb( + "https://questdb.example.com:9000") + .event_handler([](const questdb::oidc::event_view& event) { + if (event.kind() == questdb::oidc::event_kind::prompt) + std::cerr << "Open " << event.verification_uri() + << " and enter " << event.user_code() << '\n'; + }) + .build(); + + auth.sign_in(); // the only call which may prompt or open a browser + questdb::pool pool{"wss::addr=questdb.example.com:9000;", auth}; +} +``` + + + + +```c +#include +#include +#include +#include + +static void show_prompt(void *data, const questdb_oidc_event *event) { + (void)data; + if (event->kind == QUESTDB_OIDC_EVENT_PROMPT) + fprintf(stderr, "Open %.*s and enter %.*s\n", + (int)event->verification_uri_len, event->verification_uri, + (int)event->user_code_len, event->user_code); +} + +int main(void) { + questdb_error *error = NULL; + questdb_oidc_builder *builder = NULL; + questdb_oidc_auth *auth = NULL; + questdb_db *db = NULL; + int status = 1; + const char *url = "https://questdb.example.com:9000"; + builder = questdb_oidc_builder_from_questdb(url, strlen(url), &error); + if (!builder || !questdb_oidc_builder_event_handler( + builder, show_prompt, NULL, NULL, &error)) + goto done; + + auth = questdb_oidc_builder_build(builder, &error); + if (!auth || !questdb_oidc_auth_sign_in(auth, &error)) + goto done; + + questdb_db_connect_options options; + questdb_db_connect_options_init(&options, sizeof options); + options.oidc_auth = auth; + const char *conf = "wss::addr=questdb.example.com:9000;"; + db = questdb_db_connect_ex(conf, strlen(conf), &options, &error); + if (db) + status = 0; + +done: + questdb_db_close(db); + questdb_oidc_auth_free(auth); + questdb_oidc_builder_free(builder); + questdb_error_free(error); + return status; +} +``` + + + + +The pool retains the auth state and gets a cached or silently refreshed token +for every connection and reconnect. Those transport operations never prompt; +when another user approval is needed, call `sign_in()` / +`questdb_oidc_auth_sign_in()` explicitly on the main or UI thread. + +The Identity Provider must enable the device grant. For discovery without an +override, QuestDB must publish its +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint); +otherwise set the builder's issuer or configure the client explicitly. +Tokens stay in memory by default; see the [complete client +examples](/docs/security/oidc/#official-client-examples) for error handling, +endpoint pinning, and opt-in persistence across process restarts. + +### Other authentication limitations + +- Mutual TLS client certificates are not supported because the QuestDB server + does not negotiate them. Use bearer-token authentication over `wss`; see the + connect-string reference's [TLS section](/docs/connect/clients/connect-string/#tls). +- A token supplied directly through `token=...` remains fixed. Attach an OIDC + auth object for device-flow token refresh, or rebuild the pool when an + externally acquired token rotates. ## Headers diff --git a/documentation/connect/clients/java.md b/documentation/connect/clients/java.md index 1df7d8239f..5dac1f40e2 100644 --- a/documentation/connect/clients/java.md +++ b/documentation/connect/clients/java.md @@ -397,6 +397,46 @@ The token is sent as an `Authorization: Bearer YOUR_BEARER_TOKEN` header on both the ingress and egress WebSocket upgrades. It is mutually exclusive with `username`/`password`. +### OIDC device flow (Enterprise) + +`OidcDeviceAuth` signs in an interactive user with the +[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow). +It discovers the provider endpoints, client ID, scope, and the token QuestDB +expects from the server's public `/settings` endpoint. The user can approve the +sign-in from a browser on any device, so this also works from a container or +remote notebook kernel: + +```java +import io.questdb.client.QuestDB; +import io.questdb.client.cutlass.auth.OidcDeviceAuth; + +try (OidcDeviceAuth auth = OidcDeviceAuth.fromQuestDB( + "https://questdb.example.com:9000")) { + auth.signIn(); // the only call which may prompt or open a browser + + try (QuestDB db = QuestDB.connect( + "wss::addr=questdb.example.com:9000;", + auth::getToken)) { + // Ingestion and query connections share the rotating token provider. + } +} +``` + +Pass `auth::getToken` as a provider instead of putting the current token in the +connect string. Every new connection and reconnect then receives the cached or +silently refreshed token. A transport call never starts an interactive flow; +if user approval is needed again, call `signIn()` explicitly on the main or UI +thread. + +The Identity Provider must enable the device grant. For discovery without an +override, QuestDB must publish its +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint); +otherwise pin the provider with `DiscoveryOptions.issuer(...)` or configure the +client explicitly. +Tokens stay in memory by default; see the [OIDC guide's client +examples](/docs/security/oidc/#official-client-examples) for endpoint pinning, +explicit configuration, and opt-in persistence across process restarts. + ### HTTP basic auth ```java diff --git a/documentation/connect/clients/python.md b/documentation/connect/clients/python.md index d4487e1a0c..ea20c9aead 100644 --- a/documentation/connect/clients/python.md +++ b/documentation/connect/clients/python.md @@ -142,6 +142,45 @@ token = questdb.connect( ) ``` +### OIDC device flow (Enterprise) + +`OidcDeviceAuth` signs in an interactive user with the +[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow). +It discovers the provider endpoints, client ID, scope, and the token QuestDB +expects from the server's public `/settings` endpoint. The default prompt works +in terminals and remote Jupyter kernels: + +```python +import questdb +from questdb.auth import OidcDeviceAuth + +with OidcDeviceAuth.from_questdb( + "https://questdb.example.com:9000") as auth: + auth.sign_in() # the only call which may prompt or open a browser + + with questdb.connect( + "wss::addr=questdb.example.com:9000;", + oidc_auth=auth) as db: + # sender(), dataframe(), and query() share the rotating token provider. + pass +``` + +`oidc_auth=auth` keeps shared ownership of the provider and obtains the cached +or silently refreshed token for every connection and reconnect. It is mutually +exclusive with a fixed `token=` setting. Transport operations never prompt; if +they raise `OidcInteractionRequired`, call `auth.sign_in()` explicitly on the +main or UI thread. + +The Identity Provider must enable the device grant. For discovery without an +override, QuestDB must publish its +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint); +otherwise pass `issuer=...` or configure the client explicitly. +Tokens stay in memory by default. `FileTokenStore.at_default_location()` enables +opt-in persistence across restarts, but writes the refresh token as plaintext +protected by filesystem permissions. See the [OIDC client +examples](/docs/security/oidc/#official-client-examples) for the full lifecycle +and security considerations. + Authentication happens during the WebSocket upgrade, before any data frames are exchanged. Bad credentials raise `QuestDBErrorCode.AuthError` from the first operation that needs the connection, not from `connect()`. Queries and @@ -164,16 +203,13 @@ operating-system certificate store. Override it with configuration keys: See the [connect string reference](/docs/connect/clients/connect-string/) for the full grammar. -### Unsupported auth paths +### Other authentication limitations -The client supports only HTTP basic auth and static bearer-token auth. The -following are **not** supported: - -| Path | Status | Workaround | -| --- | --- | --- | -| OIDC token acquisition or in-band refresh | Not supported. The client does not negotiate with an identity provider and cannot refresh a token mid-session. | QuestDB itself supports OIDC; see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...`, and rebuild the handle when the token nears expiry. | -| Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss`. | -| Token rotation mid-session | Not supported. The handle keeps the credentials it was built with and presents them on every connection it opens — including reconnects and failover, so an expired token also breaks mid-session reconnection. | On token expiry, close the handle and build a fresh one with the new token. | +- Mutual TLS client certificates are not supported because the QuestDB server + does not negotiate them. Use bearer-token authentication over `wss`. +- A token supplied directly through `token=...` remains fixed. Use + `oidc_auth=...` for device-flow token refresh, or close the handle and build a + new one when an externally acquired token rotates. ## The pool diff --git a/documentation/connect/clients/rust.md b/documentation/connect/clients/rust.md index 40ec9e4cc3..c226ded4fa 100644 --- a/documentation/connect/clients/rust.md +++ b/documentation/connect/clients/rust.md @@ -152,6 +152,56 @@ let token = QuestDb::connect( )?; ``` +### OIDC device flow (Enterprise) + +Enable the `oidc` feature to sign in an interactive user with the +[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow): + +```toml title="Cargo.toml" +[dependencies] +questdb-rs = { version = "7", features = ["oidc"] } +``` + +`OidcDeviceAuth::from_questdb` discovers the provider endpoints, client ID, +scope, and the token QuestDB expects from the public `/settings` endpoint: + +```rust +use std::sync::Arc; +use questdb::{oidc::OidcDeviceAuth, QuestDb}; + +fn main() -> questdb::Result<()> { + let auth = Arc::new( + OidcDeviceAuth::from_questdb("https://questdb.example.com:9000") + .build()?, + ); + auth.sign_in()?; // the only call which may prompt or open a browser + + let db = QuestDb::connect_with_token_provider( + "wss::addr=questdb.example.com:9000;", + { + let auth = Arc::clone(&auth); + move || auth.token() + }, + )?; + // Sender and reader connections share the rotating token provider. + Ok(()) +} +``` + +Pass the closure as a provider instead of putting the current token in the +connect string. Every new connection and reconnect then receives the cached or +silently refreshed token. A transport call never starts an interactive flow; +if it returns `OidcErrorKind::InteractionRequired`, call `sign_in()` explicitly +on the main or UI thread. + +The Identity Provider must enable the device grant. For discovery without an +override, QuestDB must publish its +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint); +otherwise add `.issuer(...)` to the builder or configure the client explicitly. +Tokens stay in memory by default; see the [OIDC client +examples](/docs/security/oidc/#official-client-examples) for endpoint pinning +and opt-in persistence across process restarts. + With the default crate features, the TLS root set is `webpki_roots`. Other choices have feature requirements: diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 8021114f3e..175238bd9f 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -1,9 +1,11 @@ --- title: OpenID Connect (OIDC) Integration -description: Integrate QuestDB Enterprise with an external OIDC Identity Provider for Web Console SSO, PGWire token authentication, group mapping, and the settings endpoint. +description: Integrate QuestDB Enterprise with an external OIDC Identity Provider for Web Console SSO, device-flow client authentication, PGWire token authentication, and group mapping. --- import Screenshot from "@theme/Screenshot"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; import { EnterpriseNote } from "@site/src/components/EnterpriseNote" @@ -74,7 +76,9 @@ endpoints. The database, in OAuth2/OIDC terms the _protected resource_ or _resource server_. -Only processes requests which contain a valid access token. +Only processes requests which contain the bearer token selected by its OIDC +configuration: normally the access token, or the ID token when group memberships +are encoded in it. ## Authentication and Authorization Flow @@ -347,7 +351,8 @@ settings: "acl.oidc.redirect.uri": "https://questdb.host:9000", "acl.oidc.scope": "openid", "acl.oidc.authorization.endpoint": "https://oidc.provider:443/as/authorization.oauth2", - "acl.oidc.token.endpoint": "https://oidc.provider:443/as/token.oauth2" + "acl.oidc.token.endpoint": "https://oidc.provider:443/as/token.oauth2", + "acl.oidc.device.authorization.endpoint": "https://oidc.provider:443/as/device_authz.oauth2" }, "preferences.version": 0, "preferences": {} @@ -380,7 +385,8 @@ configuration document when `acl.oidc.configuration.url` is set. [`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint) is present when a device authorization endpoint has been configured, or when the provider advertises one. QuestDB publishes it without using it, for clients -which implement the Device Authorization Flow themselves. +which implement the [Device Authorization Flow](#device-authorization-flow) +themselves. `acl.oidc.redirect.uri` is `null` unless [`acl.oidc.redirect.uri`](/docs/configuration/oidc/#acloidcredirecturi) is set. @@ -431,6 +437,256 @@ is at its default, as the settings path follows the Web Console context path. A client which cannot assume both are at their defaults should make the path configurable. +## Device Authorization Flow + +The [OAuth 2.0 Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628) +signs in a person without redirecting a browser back to the client. It is a good +fit for command-line applications, containers, and remote notebook kernels: the +client displays a verification URL and short code, and the user can authorize +from a browser on any laptop or phone. + +QuestDB does not run the device flow. The client communicates directly with the +Identity Provider, then presents the resulting bearer token to QuestDB: + +```mermaid +sequenceDiagram + participant Client as QuestDB client + participant QDB as QuestDB + participant IdP as Identity Provider + participant User + Client->>QDB: GET /settings + QDB-->>Client: client ID, scope, endpoints, token mode + Client->>IdP: Request device and user codes + IdP-->>Client: device_code, user_code, verification URI + Client-->>User: Display URL and code + User->>IdP: Sign in and approve in a browser + loop Until approved or expired + Client->>IdP: Poll token endpoint + IdP-->>Client: authorization_pending / tokens + end + Client->>QDB: Authorization: Bearer selected token +``` + +The client must respect the polling interval, `slow_down` responses, and the +device code's expiry. The official clients handle those protocol details, +choose the access or ID token according to +[`acl.oidc.groups.encoded.in.token`](#which-token-to-send), cache it in memory, +and refresh it silently when the provider issues a refresh token. They never +send the device code or the user's Identity Provider password to QuestDB. + +### Configure the provider and QuestDB + +Before using the flow: + +1. Enable the device authorization grant for the public client registered with + the Identity Provider. The discovery examples below use the client ID in + `acl.oidc.client.id`; configure the client explicitly when an application + has its own registration. +2. Make the device authorization endpoint available to clients. For the + zero-configuration examples below, `acl.oidc.configuration.url` must resolve + a provider document containing `device_authorization_endpoint`, or the + host-based configuration must set + [`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint). +3. Keep `openid` in `acl.oidc.scope`. Add `offline_access` if your provider + requires that scope before issuing a refresh token to a device-flow client. + +For example, a host-based configuration can add: + +```ini title="server.conf" +acl.oidc.scope=openid offline_access +acl.oidc.device.authorization.endpoint=/as/device_authz.oauth2 +``` + +The exact endpoint path and refresh-token scope are provider-specific. With +configuration-document discovery, do not set the endpoint property: all +individual endpoint settings are ignored in that mode. + +If QuestDB does not publish the device authorization endpoint, pin the provider +with the client's `issuer` option so the client can fetch the provider's +`/.well-known/openid-configuration` document, or configure the client ID and +both endpoints explicitly. + +### Official client examples + +Each example discovers the OIDC client ID, scope, token-selection mode, and +provider endpoints from QuestDB's [settings endpoint](#settings-endpoint), then +signs in before opening the database connection. Use an `https` QuestDB URL for +discovery so an attacker cannot replace the advertised Identity Provider +endpoints. + + + + +```java +import io.questdb.client.QuestDB; +import io.questdb.client.cutlass.auth.OidcDeviceAuth; + +try (OidcDeviceAuth auth = OidcDeviceAuth.fromQuestDB( + "https://questdb.example.com:9000")) { + auth.signIn(); // the only call which may prompt or open a browser + + try (QuestDB db = QuestDB.connect( + "wss::addr=questdb.example.com:9000;", + auth::getToken)) { + // db.borrowSender() and db.borrowQuery() use the rotating token. + } +} +``` + + + + +```python +import questdb +from questdb.auth import OidcDeviceAuth + +with OidcDeviceAuth.from_questdb( + "https://questdb.example.com:9000") as auth: + auth.sign_in() # the only call which may prompt or open a browser + + with questdb.connect( + "wss::addr=questdb.example.com:9000;", + oidc_auth=auth) as db: + # db.sender(), db.dataframe(), and db.query() use the rotating token. + pass +``` + + + + +Enable the `oidc` crate feature: + +```toml title="Cargo.toml" +[dependencies] +questdb-rs = { version = "7", features = ["oidc"] } +``` + +```rust +use std::sync::Arc; +use questdb::{oidc::OidcDeviceAuth, QuestDb}; + +fn main() -> questdb::Result<()> { + let auth = Arc::new( + OidcDeviceAuth::from_questdb("https://questdb.example.com:9000") + .build()?, + ); + auth.sign_in()?; // the only call which may prompt or open a browser + + let db = QuestDb::connect_with_token_provider( + "wss::addr=questdb.example.com:9000;", + { + let auth = Arc::clone(&auth); + move || auth.token() + }, + )?; + // db.borrow_sender() and db.borrow_reader() use the rotating token. + Ok(()) +} +``` + + + + +```cpp +#include +#include +#include + +int main() { + auto auth = questdb::oidc::builder::from_questdb( + "https://questdb.example.com:9000") + .event_handler([](const questdb::oidc::event_view& event) { + if (event.kind() == questdb::oidc::event_kind::prompt) + std::cerr << "Open " << event.verification_uri() + << " and enter " << event.user_code() << '\n'; + }) + .build(); + + auth.sign_in(); // the only call which may prompt or open a browser + questdb::pool pool{"wss::addr=questdb.example.com:9000;", auth}; + // pool.borrow_sender() and pool.borrow_reader() use the rotating token. +} +``` + + + + +```c +#include +#include +#include +#include + +static void show_prompt(void *data, const questdb_oidc_event *event) { + (void)data; + if (event->kind == QUESTDB_OIDC_EVENT_PROMPT) + fprintf(stderr, "Open %.*s and enter %.*s\n", + (int)event->verification_uri_len, event->verification_uri, + (int)event->user_code_len, event->user_code); +} + +int main(void) { + questdb_error *error = NULL; + questdb_oidc_auth *auth = NULL; + const char *url = "https://questdb.example.com:9000"; + questdb_oidc_builder *builder = + questdb_oidc_builder_from_questdb(url, strlen(url), &error); + if (!builder || !questdb_oidc_builder_event_handler( + builder, show_prompt, NULL, NULL, &error)) + goto fail; + + auth = questdb_oidc_builder_build(builder, &error); + if (!auth || !questdb_oidc_auth_sign_in(auth, &error)) + goto fail; + + questdb_db_connect_options options; + questdb_db_connect_options_init(&options, sizeof options); + options.oidc_auth = auth; + + const char *conf = "wss::addr=questdb.example.com:9000;"; + questdb_db *db = questdb_db_connect_ex( + conf, strlen(conf), &options, &error); + if (!db) + goto fail; + + /* Sender and reader borrows use the rotating token. */ + questdb_db_close(db); + questdb_oidc_auth_free(auth); + questdb_oidc_builder_free(builder); + return 0; + +fail: + if (error) { + size_t len = 0; + const char *message = questdb_error_msg(error, &len); + fprintf(stderr, "OIDC sign-in failed: %.*s\n", (int)len, message); + } + questdb_error_free(error); + questdb_oidc_auth_free(auth); + questdb_oidc_builder_free(builder); + return 1; +} +``` + + + + +The authentication object owns the token state. Pass it as a rotating provider, +as shown above, instead of copying the current token into the connection string; +otherwise a reconnect after token expiry will keep sending the stale value. +Interactive sign-in belongs on the main or UI thread. Transport operations may +refresh silently, but return an interaction-required error when a new user +authorization is necessary. + +Tokens remain in memory by default. Java, Python, Rust, C, and C++ also offer an +opt-in file token store for avoiding a new prompt after process restarts. The +store contains a long-lived refresh token as plaintext protected by filesystem +permissions; enable it only when that tradeoff is acceptable. + +Device flow always requires a person to approve the sign-in. For unattended +services, scheduled jobs, or CI, use a service-account token or a provider flow +intended for machine identities instead. + ## Interactive clients Any interactive client - a UI, Jupyter notebook, CLI - can integrate with an @@ -466,8 +722,9 @@ special version of the Authorization Code flow designed for mobile apps and single page applications. Regardless of which flow is used by the web or mobile application, the requested -access token can be used for authentication and authorization when communicating -with QuestDB as explained in the [above 7th step](#7-database-access). +token can be used for authentication and authorization when communicating with +QuestDB. Select the access or ID token as described in +[Which token to send](#which-token-to-send). ### Jupyter notebook @@ -478,11 +735,14 @@ The OAuthenticator documentation also contains [examples](https://oauthenticator.readthedocs.io/en/latest/tutorials/provider-specific-setup/index.html) using different identity providers. -If Jupyter notebooks are used without JupyterHub, one option for OAuth2 -integration is to use the -Resource Owner Password Credentials (ROPC) flow. -It is likely that enabling this flow in your OAuth2 provider will require -additional setup. +If Jupyter notebooks are used without JupyterHub, use the official Python +client's [Device Authorization Flow](#device-authorization-flow). It works when +the browser and notebook kernel are on different machines and does not put the +user's password in the notebook. + +For tooling without device-flow support, a last-resort option is the +Resource Owner Password Credentials (ROPC) flow. Enabling it usually +requires additional provider configuration. :::caution @@ -490,7 +750,13 @@ The Resource Owner Password Credentials flow is legacy, and should be used as a ::: -We can use the code below to acquire an access token in our notebook: +The ROPC snippets below assume +`acl.oidc.groups.encoded.in.token=false` and therefore send the access token. If +the setting is `true`, the client must send the ID token instead, and the +provider must issue one for the password grant. The official device-flow client +selects the correct token automatically. + +As a fallback, the code below acquires an access token with ROPC: ```python from urllib import request, parse @@ -634,8 +900,12 @@ with pg.connect(conn_str, autocommit=True) as connection: ### CLI, standalone applications -When using CLI tools, such as `psql`, or standalone applications like Microsoft -Access, the best option may be the Resource Owner Password Credentials flow. +For a CLI or standalone application built with an official Java, Rust, C, or +C++ client, use the [Device Authorization Flow](#device-authorization-flow). +Tools such as `psql` and Microsoft Access cannot run the flow themselves; for +those tools, acquire a token separately and use +[PGWire token authentication](#oidc-for-the-pgwire-endpoint), or enable the +legacy Resource Owner Password Credentials flow. The user logs in with their SSO credentials, and the server validates the details with the OAuth2 provider: From f2963720c71483704b5a239010d9084579124e70 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Thu, 3 Sep 2026 00:39:58 +0100 Subject: [PATCH 34/38] Fix three security gaps in the OIDC guide The ROPC example discovered the provider's token endpoint over plaintext http and then posted the user's SSO password to whatever URL came back, contradicting the rule the same page states for the device flow. Both snippets now use https against questdb.example.com, matching the device-flow examples, and a caution explains why the discovery request and the connection carrying the token both need it. "Which token to send" is the section every client page links to, but the consequence of acl.oidc.groups.encoded.in.token lived only on the configuration page and in the Entra ID walkthrough. It now carries its own caution: exp, nbf and iss go unchecked, so an expired token is still accepted and provider token lifetimes are not enforced. The validation column also named two checks where the server performs three. https for discovery was written as advice; the clients enforce it. Say so, and name the opt-out for each language. Note that Rust, C, C++ and Python accept loopback http for local development while Java rejects it, since that difference decides whether http://localhost:9000 works. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY --- documentation/security/oidc.mdx | 43 +++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 175238bd9f..0ddab5a8ca 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -405,7 +405,7 @@ by the provider belongs in the `Authorization: Bearer` header: | Value | Token to send | How QuestDB validates it | | --- | --- | --- | | `false`, the default | the access token | by calling the user info endpoint | -| `true` | the ID token | locally, by checking its signature and audience | +| `true` | the ID token | locally, by checking its signature, audience, and claims | Sending the wrong one fails authentication with `401 Unauthorized` and the reason in the server log. With @@ -415,6 +415,18 @@ straight out of its payload. The [Web Console](/docs/getting-started/web-console/overview/) picks the token this way, and so should any other client. +:::caution + +With `acl.oidc.groups.encoded.in.token` enabled, QuestDB checks the token's +signature, its `aud` claim, and that `sub` and the group memberships are +present. It does not check `exp`, `nbf` or `iss`, so an expired token is still +accepted, and the provider's token lifetimes are not enforced. Do not enable the +setting where you rely on being able to revoke a token. See +[`acl.oidc.groups.encoded.in.token`](/docs/configuration/oidc/#acloidcgroupsencodedintoken) +for the full list of what is and is not validated. + +::: + :::note Read `acl.oidc.enabled` before any of the other keys. The `acl.oidc.*` entries @@ -510,9 +522,17 @@ both endpoints explicitly. Each example discovers the OIDC client ID, scope, token-selection mode, and provider endpoints from QuestDB's [settings endpoint](#settings-endpoint), then -signs in before opening the database connection. Use an `https` QuestDB URL for -discovery so an attacker cannot replace the advertised Identity Provider -endpoints. +signs in before opening the database connection. + +The discovery URL must use `https`: reaching QuestDB over plaintext would let an +attacker replace the advertised Identity Provider endpoints and redirect the +device code and the refresh token, so the clients reject it rather than warn. +The Rust, C, C++ and Python clients accept loopback `http` for local +development; the Java client does not. To use a plaintext non-loopback host +anyway, opt in with `allowInsecureTransport(true)` (Java), +`.allow_insecure_transport(true)` (Rust), +`questdb_oidc_builder_allow_insecure_transport()` (C), +`allow_insecure_transport()` (C++) or `insecure=True` (Python). @@ -939,6 +959,15 @@ application which integrates via OIDC should be given a different Client Id, so that the two can be told apart in the provider's audit log and given different policies. +:::caution + +Both requests must go over `https`. The settings response names the URL this +code posts the user's password to, so anyone able to tamper with a plaintext +response can redirect the password grant to a host of their choosing. The same +applies to the connection below, which carries the resulting token. + +::: + ```python import os import requests @@ -955,7 +984,7 @@ pwd = os.environ.get("password") client_id = os.environ.get("client_id") scope = "openid" -settings = requests.get("http://localhost:9000/settings").json()["config"] +settings = requests.get("https://questdb.example.com:9000/settings").json()["config"] if not settings.get("acl.oidc.enabled"): raise SystemExit("OIDC is not enabled on this QuestDB instance") @@ -975,7 +1004,7 @@ if token_key not in tokens: raise SystemExit(f"the provider did not issue an {token_key} for the password grant") token = tokens[token_key] -conf = f"http::addr=localhost:9000;token={token};" +conf = f"https::addr=questdb.example.com:9000;token={token};" with Sender.from_conf(conf) as sender: df = pd.read_csv("data.csv") df["ts"] = pd.to_datetime(df["ts"]) @@ -1006,7 +1035,7 @@ load_dotenv() user = os.environ.get("username") pwd = os.environ.get("password") -conf = f"http::addr=localhost:9000;username={user};password={pwd};" +conf = f"https::addr=questdb.example.com:9000;username={user};password={pwd};" with Sender.from_conf(conf) as sender: df = pd.read_csv("data.csv") df["ts"] = pd.to_datetime(df["ts"]) From 0ad71781f8f01e7de0f0ccd02b46a92ef84d9061 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Thu, 3 Sep 2026 00:49:18 +0100 Subject: [PATCH 35/38] Name the OIDC device-flow APIs the docs tell you to use The four client pages sent readers to #official-client-examples for endpoint pinning, explicit configuration, persistence and error handling. That section held five copies of the same happy path and none of those things, so the pointers dead-ended. It now carries four subsections covering them, and the pointers name what is actually there. Each subsection names the API per language rather than describing it: - The sign-in prompt. The five clients behave differently and the tabs implied they did not: Java prints to System.out and performs no terminal check, so redirecting output hides the code; Rust, C and C++ refuse without a TTY; Python defers to the IPython kernel. Records the custom-prompt and force-interactive setters, and which URL is vetted for opening. - Explicit configuration. "Configure the client explicitly" appeared on five pages and named nothing. Lists what each setting is for and where the setters live, including that Java splits them between DiscoveryOptions and builder(). Notes that acl.oidc.audience is never published, so discovery cannot supply it and a provider that needs an audience requires an explicit one. - Token persistence. FileTokenStore was named once, in prose, with no wiring. Gives the per-language constructor and attachment, the default directory and its override, and the 0600/0700 permissions, so the tradeoff the page asks the reader to accept can actually be judged. - Interaction-required errors. Java has no distinct type and Rust flattens the kind to ErrorCode::AuthError, reachable only through err.oidc_error(), so the blanket claim that transports "return an interaction-required error" was wrong for two of the five clients. Also corrects DiscoveryOptions.issuer(...) on the Java page, which reads as a static call on a top-level type and is neither, and notes on all four pages that silent refresh depends on offline_access being in acl.oidc.scope. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY --- documentation/connect/clients/c-and-cpp.md | 28 ++++-- documentation/connect/clients/java.md | 29 ++++-- documentation/connect/clients/python.md | 28 +++--- documentation/connect/clients/rust.md | 28 ++++-- documentation/security/oidc.mdx | 103 +++++++++++++++++++-- 5 files changed, 169 insertions(+), 47 deletions(-) diff --git a/documentation/connect/clients/c-and-cpp.md b/documentation/connect/clients/c-and-cpp.md index f2830ece6d..fc53123385 100644 --- a/documentation/connect/clients/c-and-cpp.md +++ b/documentation/connect/clients/c-and-cpp.md @@ -410,17 +410,27 @@ done: The pool retains the auth state and gets a cached or silently refreshed token -for every connection and reconnect. Those transport operations never prompt; -when another user approval is needed, call `sign_in()` / -`questdb_oidc_auth_sign_in()` explicitly on the main or UI thread. +for every connection and reconnect. Silent refresh needs a refresh token, which +most providers issue only when `offline_access` is among the scopes in +[`acl.oidc.scope`](/docs/configuration/oidc/#acloidcscope) or in the client's +own `scope` override. Those transport operations never prompt; they fail with +`QUESTDB_OIDC_ERROR_INTERACTION_REQUIRED` in C, or +`questdb::error_kind::interaction_required` in C++, and the application calls +`sign_in()` / `questdb_oidc_auth_sign_in()` explicitly on the main or UI thread. The Identity Provider must enable the device grant. For discovery without an -override, QuestDB must publish its -[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint); -otherwise set the builder's issuer or configure the client explicitly. -Tokens stay in memory by default; see the [complete client -examples](/docs/security/oidc/#official-client-examples) for error handling, -endpoint pinning, and opt-in persistence across process restarts. +override, QuestDB must publish a device authorization endpoint, either from the +provider's configuration document or from +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint). +Otherwise set the builder's `issuer`, or set `client_id`, `scope`, `audience`, +`token_endpoint` and `device_authorization_endpoint` yourself; in C the same +names are prefixed with `questdb_oidc_builder_`. + +Tokens stay in memory until `.default_file_token_store()` in C++, or +`questdb_oidc_builder_default_file_token_store()` in C, writes a long-lived +refresh token to disk as plaintext. See the [complete client +examples](/docs/security/oidc/#official-client-examples) for error handling, the +sign-in prompt, and the store's location and permissions. ### Other authentication limitations diff --git a/documentation/connect/clients/java.md b/documentation/connect/clients/java.md index 5dac1f40e2..cc963c1dd2 100644 --- a/documentation/connect/clients/java.md +++ b/documentation/connect/clients/java.md @@ -424,18 +424,27 @@ try (OidcDeviceAuth auth = OidcDeviceAuth.fromQuestDB( Pass `auth::getToken` as a provider instead of putting the current token in the connect string. Every new connection and reconnect then receives the cached or -silently refreshed token. A transport call never starts an interactive flow; -if user approval is needed again, call `signIn()` explicitly on the main or UI -thread. +silently refreshed token. Silent refresh needs a refresh token, which most +providers issue only when `offline_access` is among the scopes in +[`acl.oidc.scope`](/docs/configuration/oidc/#acloidcscope) or in the client's +own `scope` override. A transport call never starts an interactive flow; it +throws `OidcAuthException`, which carries no distinct interaction-required type, +so call `signIn()` again on the main or UI thread. The Identity Provider must enable the device grant. For discovery without an -override, QuestDB must publish its -[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint); -otherwise pin the provider with `DiscoveryOptions.issuer(...)` or configure the -client explicitly. -Tokens stay in memory by default; see the [OIDC guide's client -examples](/docs/security/oidc/#official-client-examples) for endpoint pinning, -explicit configuration, and opt-in persistence across process restarts. +override, QuestDB must publish a device authorization endpoint, either from the +provider's configuration document or from +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint). +Otherwise pin the provider by passing +`new OidcDeviceAuth.DiscoveryOptions().issuer("https://idp")` as the second +argument to `fromQuestDB`, or set the client ID, scope and endpoints yourself +with `OidcDeviceAuth.builder()`. + +Tokens stay in memory until a store is attached with +`DiscoveryOptions.tokenStore(FileTokenStore.atDefaultLocation())`, which writes +a long-lived refresh token to disk as plaintext. See the [OIDC guide's client +examples](/docs/security/oidc/#official-client-examples) for the sign-in prompt, +explicit configuration, and the token store's location and permissions. ### HTTP basic auth diff --git a/documentation/connect/clients/python.md b/documentation/connect/clients/python.md index ea20c9aead..3b78289a15 100644 --- a/documentation/connect/clients/python.md +++ b/documentation/connect/clients/python.md @@ -167,19 +167,25 @@ with OidcDeviceAuth.from_questdb( `oidc_auth=auth` keeps shared ownership of the provider and obtains the cached or silently refreshed token for every connection and reconnect. It is mutually -exclusive with a fixed `token=` setting. Transport operations never prompt; if -they raise `OidcInteractionRequired`, call `auth.sign_in()` explicitly on the -main or UI thread. +exclusive with a fixed `token=` setting. Silent refresh needs a refresh token, +which most providers issue only when `offline_access` is among the scopes in +[`acl.oidc.scope`](/docs/configuration/oidc/#acloidcscope) or in the client's +own `scope` override. Transport operations never prompt; if they raise +`OidcInteractionRequired`, importable from `questdb.auth`, call `auth.sign_in()` +explicitly on the main or UI thread. The Identity Provider must enable the device grant. For discovery without an -override, QuestDB must publish its -[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint); -otherwise pass `issuer=...` or configure the client explicitly. -Tokens stay in memory by default. `FileTokenStore.at_default_location()` enables -opt-in persistence across restarts, but writes the refresh token as plaintext -protected by filesystem permissions. See the [OIDC client -examples](/docs/security/oidc/#official-client-examples) for the full lifecycle -and security considerations. +override, QuestDB must publish a device authorization endpoint, either from the +provider's configuration document or from +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint). +Otherwise pass `issuer=...`, or set `client_id=`, `scope=`, `audience=`, +`token_endpoint=` and `device_authorization_endpoint=` on `from_questdb`. + +Tokens stay in memory until a store is passed as +`token_store=FileTokenStore.at_default_location()`, which writes a long-lived +refresh token to disk as plaintext. See the [OIDC client +examples](/docs/security/oidc/#official-client-examples) for the sign-in prompt, +explicit configuration, and the store's location and permissions. Authentication happens during the WebSocket upgrade, before any data frames are exchanged. Bad credentials raise `QuestDBErrorCode.AuthError` from the diff --git a/documentation/connect/clients/rust.md b/documentation/connect/clients/rust.md index c226ded4fa..63823bf02e 100644 --- a/documentation/connect/clients/rust.md +++ b/documentation/connect/clients/rust.md @@ -190,17 +190,27 @@ fn main() -> questdb::Result<()> { Pass the closure as a provider instead of putting the current token in the connect string. Every new connection and reconnect then receives the cached or -silently refreshed token. A transport call never starts an interactive flow; -if it returns `OidcErrorKind::InteractionRequired`, call `sign_in()` explicitly -on the main or UI thread. +silently refreshed token. Silent refresh needs a refresh token, which most +providers issue only when `offline_access` is among the scopes in +[`acl.oidc.scope`](/docs/configuration/oidc/#acloidcscope) or in the client's +own `scope` override. A transport call never starts an interactive flow; it +returns a `questdb::Error` with `ErrorCode::AuthError`, whose `err.oidc_error()` +reports `OidcErrorKind::InteractionRequired` when a new sign-in is needed. Call +`sign_in()` explicitly on the main or UI thread. The Identity Provider must enable the device grant. For discovery without an -override, QuestDB must publish its -[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint); -otherwise add `.issuer(...)` to the builder or configure the client explicitly. -Tokens stay in memory by default; see the [OIDC client -examples](/docs/security/oidc/#official-client-examples) for endpoint pinning -and opt-in persistence across process restarts. +override, QuestDB must publish a device authorization endpoint, either from the +provider's configuration document or from +[`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint). +Otherwise add `.issuer(...)` to the builder, or set `.client_id()`, `.scope()`, +`.audience()`, `.token_endpoint()` and `.device_authorization_endpoint()` +yourself. + +Tokens stay in memory until +`.token_store(FileTokenStore::at_default_location()?)` is added, which writes a +long-lived refresh token to disk as plaintext. See the [OIDC client +examples](/docs/security/oidc/#official-client-examples) for the sign-in prompt, +explicit configuration, and the store's location and permissions. With the default crate features, the TLS root set is `webpki_roots`. Other choices have feature requirements: diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 0ddab5a8ca..168f459ab1 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -694,19 +694,106 @@ fail: The authentication object owns the token state. Pass it as a rotating provider, as shown above, instead of copying the current token into the connection string; otherwise a reconnect after token expiry will keep sending the stale value. -Interactive sign-in belongs on the main or UI thread. Transport operations may -refresh silently, but return an interaction-required error when a new user -authorization is necessary. - -Tokens remain in memory by default. Java, Python, Rust, C, and C++ also offer an -opt-in file token store for avoiding a new prompt after process restarts. The -store contains a long-lived refresh token as plaintext protected by filesystem -permissions; enable it only when that tradeoff is acceptable. Device flow always requires a person to approve the sign-in. For unattended services, scheduled jobs, or CI, use a service-account token or a provider flow intended for machine identities instead. +#### The sign-in prompt + +Signing in renders the verification URL and the user code, then blocks until the +user approves or the device code expires. Where that text goes, and whether the +call runs at all without a terminal, differs by client: + +| Client | Default prompt | Without a terminal | +| --- | --- | --- | +| Java | prints to `System.out`, then tries to open a browser | runs anyway; the client performs no terminal check | +| Python | prints to stderr, or to the frontend inside an IPython kernel | fails, unless the kernel accepts stdin | +| Rust, C, C++ | prints to stderr | fails with an interaction-required error | + +Redirecting output therefore hides the code in Java, while a container with no +TTY fails outright in the other four. Override the default to render the +challenge elsewhere, or to declare the process interactive anyway: + +| Client | Custom prompt | Force interactive | +| --- | --- | --- | +| Java | `prompt(...)`, or `DeviceCodePrompt.SYSTEM_OUT` to skip the browser launch | not applicable | +| Python | `renderer=` | `interactive=True` | +| Rust | `.renderer(...)` | `.interactive(true)` | +| C | `questdb_oidc_builder_event_handler()` | `questdb_oidc_builder_interactive()` | +| C++ | `.event_handler(...)` | `.interactive(true)` | + +The prompt's text fields are display-safe. Where a client opens the URL or makes +it clickable, use the separately vetted browser target instead: `browser_target` +on the C event struct, `event_view::browser_target()` in C++. + +#### Explicit configuration and endpoint pinning + +Discovery supplies the client ID, scope, audience, token-selection mode, and +endpoints. Set any of them explicitly when the application has its own +registration with the provider, or when QuestDB does not publish what the client +needs: + +| Setting | Set it when | +| --- | --- | +| `client_id` | the application has its own registration, which is the recommended setup | +| `scope` | the provider requires `offline_access` before it issues a refresh token | +| `audience` | the provider requires one, or QuestDB validates `aud` against a non-default [`acl.oidc.audience`](/docs/configuration/oidc/#acloidcaudience). QuestDB does not publish that key, so discovery cannot supply it | +| `issuer` | QuestDB publishes no device authorization endpoint, so the client reads the provider's `/.well-known/openid-configuration` instead | +| `token_endpoint`, `device_authorization_endpoint` | pinning both endpoints directly rather than through an issuer | +| `groups_in_token` | overriding which of the two tokens the client selects | + +Java exposes `issuer`, `prompt`, `tokenStore`, `tlsConfig` and +`allowInsecureTransport` on `OidcDeviceAuth.DiscoveryOptions`, and the full set +on `OidcDeviceAuth.builder()`, in camel case. Python passes them as keyword +arguments to `from_questdb`. Rust and C++ are builder methods in snake case, and +C prefixes the same names with `questdb_oidc_builder_`. + +#### Token persistence + +Tokens stay in memory unless a file token store is enabled. The store writes the +access, ID and long-lived refresh tokens as unencrypted JSON, so turn it on only +where that at-rest exposure is acceptable: + +| Client | Default location | A directory you choose | +| --- | --- | --- | +| Java | `FileTokenStore.atDefaultLocation()` | `FileTokenStore.at(path)` | +| Python | `FileTokenStore.at_default_location()` | `FileTokenStore.at(path)` | +| Rust | `FileTokenStore::at_default_location()?` | `FileTokenStore::at(path)` | +| C | `questdb_oidc_builder_default_file_token_store()` | `questdb_oidc_builder_file_token_store()` | +| C++ | `.default_file_token_store()` | `.file_token_store(directory)` | + +Java, Python and Rust attach the store with `tokenStore(...)`, `token_store=` +and `.token_store(...)` respectively; the C and C++ calls above are builder +methods and attach it themselves. + +The default directory is `$HOME/.questdb/oidc-tokens/`, overridden by the +`questdb.client.oidc.token.store.dir` environment variable. Java reads that same +name as a JVM system property rather than an environment variable, and falls +back to `${user.home}/.questdb/oidc-tokens/`. On Unix the clients create token +files with mode `0600` and the store directory with `0700`; on other platforms +the directory's existing ACL governs access, so restrict it before enabling the +store. + +#### Interaction-required errors + +A transport call never prompts. When the cached token cannot be refreshed +silently, the call fails instead, and the application has to sign in again on +the main or UI thread: + +| Client | How it surfaces | +| --- | --- | +| Python | `OidcInteractionRequired`, importable from `questdb.auth` | +| Rust | a `questdb::Error` carrying `ErrorCode::AuthError`; read `err.oidc_error()` and match on `OidcErrorKind::InteractionRequired` | +| C | error kind `QUESTDB_OIDC_ERROR_INTERACTION_REQUIRED` | +| C++ | `questdb::error_kind::interaction_required` | +| Java | `OidcAuthException`, with no distinct interaction-required type to match on | + +In C++ a failure raised through an attached sender arrives as +`questdb::ingress::line_sender_error` carrying `oidc_diagnostic()`, not as +`questdb::oidc::error`, so catch the common base `const questdb::error&` to +handle both. + ## Interactive clients Any interactive client - a UI, Jupyter notebook, CLI - can integrate with an From c366b4bec52d8522b324d9c4ac10049fb5f09358 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Thu, 3 Sep 2026 01:02:00 +0100 Subject: [PATCH 36/38] Reconcile OIDC across the pages that contradict it Six places where the docs disagreed with themselves or with the server. Auth enumerations that predate OIDC now include it. security/rbac.md counted three authentication methods and connect/compatibility/rest-api.md counted two; both are canonical pages that other pages defer to, and both omitted the credential this PR documents. rest-api.md also now says an OIDC bearer token is a different credential from the REST API token it describes below, since the two share the Authorization: Bearer header and are easy to confuse. connect/clients/connect-string.md is where the client pages send readers for the full credential grammar, and it listed only username, password and token. It now records that device-flow credentials are not connect-string keys: the auth object rides alongside, and rotates, where a static token= does not. high-availability/failover.md told operators to send "an OIDC access token". Under acl.oidc.groups.encoded.in.token, which the Entra ID walkthrough in this same PR sets to true, that token is rejected with a 401. It now names both tokens and links the rule. The Client Id rule had three answers. The architecture overview and the non-interactive section both say each application should have its own; the device-flow examples reuse QuestDB's without saying so. The examples now say what discovery costs, and step 1 states the rule once and points at the explicit-configuration section instead of gesturing at it. Go and .NET readers could reach the settings endpoint and no further: the sections they landed on pointed back at each other, and neither page linked the one section describing a flow they could implement. Both now do, and name a library that implements the device grant. Finally, the token-flow validator deserializes the payload into a fixed struct with sub and groups, so it requires those literal names and rejects a token without them whatever acl.oidc.sub.claim and acl.oidc.groups.claim say. Custom names only ever applied to the user info flow. Both option entries now say so. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY --- documentation/configuration/oidc.md | 12 +++++++++++ .../connect/clients/connect-string.md | 6 ++++++ documentation/connect/clients/dotnet.md | 2 +- documentation/connect/clients/go.md | 6 ++++++ .../connect/compatibility/rest-api.md | 7 ++++++- documentation/high-availability/failover.md | 4 +++- documentation/security/oidc.mdx | 15 +++++++++---- documentation/security/rbac.md | 21 ++++++++++++------- 8 files changed, 59 insertions(+), 14 deletions(-) diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 849dc66212..c0b6e30085 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -356,6 +356,10 @@ If the claim is missing from the user information, or it is an empty list, authentication fails. See [Mapping user permissions](/docs/security/oidc/#mapping-user-permissions). +A custom name applies to the user info flow. Set this to `groups` when +[`acl.oidc.groups.encoded.in.token`](#acloidcgroupsencodedintoken) is `true`, +because the token validator requires that name. + ### acl.oidc.groups.encoded.in.token - **Default**: `false` @@ -382,6 +386,14 @@ the token itself and never asks the provider about it: | `aud`, against [`acl.oidc.audience`](#acloidcaudience) | `nbf`, the not-before time | | that `sub` and the group memberships are present | `iss`, the issuer | +The validator reads those two claims under the names `sub` and `groups` +literally, and rejects a token which does not carry both, whatever +[`acl.oidc.sub.claim`](#acloidcsubclaim) and +[`acl.oidc.groups.claim`](#acloidcgroupsclaim) are set to. `groups` must be an +array of strings. Custom claim names apply to the user info flow, so with this +setting enabled leave `acl.oidc.sub.claim` at its default and set +`acl.oidc.groups.claim=groups`. + :::caution An expired token is therefore still accepted. Once issued, a token stays valid diff --git a/documentation/connect/clients/connect-string.md b/documentation/connect/clients/connect-string.md index c027700dd5..4283cee237 100644 --- a/documentation/connect/clients/connect-string.md +++ b/documentation/connect/clients/connect-string.md @@ -220,6 +220,12 @@ WebSocket upgrade request. exclusive with `username` / `password`. Token auth avoids the per-request overhead of basic auth and is the recommended path for Enterprise deployments. +- Device-flow OIDC credentials are **not** connect-string keys. The Java, + Python, Rust, C and C++ clients take an auth object alongside the connect + string, which rotates the token on every reconnect; see + [OIDC device flow](/docs/security/oidc/#device-authorization-flow). A + static `token=` does not rotate, so a reconnect after expiry keeps sending + the stale value. - `auth_timeout_ms` — per-host upper bound on the upgrade response read. Does not cover TLS handshake or post-upgrade frame reads, which use OS or hard-coded defaults. Default: `15000` (15 s). diff --git a/documentation/connect/clients/dotnet.md b/documentation/connect/clients/dotnet.md index cc07628164..c36dada8bf 100644 --- a/documentation/connect/clients/dotnet.md +++ b/documentation/connect/clients/dotnet.md @@ -228,7 +228,7 @@ that purpose; left unset, it inherits `auth_timeout_ms`. | Path | Status | Workaround | |---|---|---| -| OIDC token acquisition or in-band refresh | Not supported by this client. It does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC — see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...` above, and rebuild the sender / query client when the token nears expiry. | +| OIDC token acquisition or in-band refresh | Not supported by this client. It does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC — see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...` above, and rebuild the sender / query client when the token nears expiry. To acquire one, run the [Device Authorization Flow](/docs/security/oidc/#device-authorization-flow) with an OAuth2 library such as MSAL.NET; QuestDB publishes the device authorization endpoint on the same settings response. | | Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss://`. See the connect-string reference for the canonical statement. | | Token rotation mid-session | Not supported. Credentials are presented once during the WebSocket upgrade and are not re-sent. | On token expiry, `await sender.DisposeAsync()` and build a fresh sender with the new token. The same applies to `QueryClient`. | diff --git a/documentation/connect/clients/go.md b/documentation/connect/clients/go.md index 2c552d8bee..086781b576 100644 --- a/documentation/connect/clients/go.md +++ b/documentation/connect/clients/go.md @@ -367,6 +367,12 @@ QuestDB publishes the provider's authorization and token endpoints on its discover them instead of hard coding them. The same response tells the client [which token to send](/docs/security/oidc/#which-token-to-send): the access token, or the ID token when QuestDB reads group memberships from the token. +This client does not run an OIDC flow, so acquire the token with an OAuth2 +library: `golang.org/x/oauth2` implements the device grant, and QuestDB +publishes the device authorization endpoint on the same settings response. The +[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow) +describes the protocol, including the polling interval, `slow_down`, and the +device code's expiry. When the token expires or is rotated, construct a new handle with the new token. An expired or rejected token surfaces as an authentication failure (see [Connection-level errors](#connection-level-errors)). It is mutually exclusive diff --git a/documentation/connect/compatibility/rest-api.md b/documentation/connect/compatibility/rest-api.md index 033060ff2c..c1e6a1b573 100644 --- a/documentation/connect/compatibility/rest-api.md +++ b/documentation/connect/compatibility/rest-api.md @@ -794,11 +794,16 @@ A HTTP status code of `400` is returned with the following response body: ## Authentication -The REST API supports two authentication types: +The REST API supports three authentication types: - **HTTP basic authentication**, available in QuestDB Open Source and QuestDB Enterprise. - **Token-based authentication**, available in QuestDB Enterprise only. +- **OIDC bearer tokens**, available in QuestDB Enterprise only. These also + travel in the `Authorization: Bearer` header but are issued by an external + Identity Provider rather than by QuestDB, so they are a different credential + from the REST API token below. See [OpenID Connect](/docs/security/oidc/) and + [which token to send](/docs/security/oidc/#which-token-to-send). :::note diff --git a/documentation/high-availability/failover.md b/documentation/high-availability/failover.md index 93f58855d3..67345bc76d 100644 --- a/documentation/high-availability/failover.md +++ b/documentation/high-availability/failover.md @@ -284,7 +284,9 @@ The [minimal HTTP server](/docs/operations/logging-metrics/#minimal-http-server) on port 9003 exposes the same switch to external coordinators. It accepts the same credentials as the main HTTP server: HTTP basic authentication, a [REST token](/docs/connect/compatibility/rest-api/#authentication-via-token-in-questdb-enterprise) -as `Authorization: Bearer`, or an OIDC access token. The principal needs the +as `Authorization: Bearer`, or an OIDC token — [the access token or the ID +token](/docs/security/oidc/#which-token-to-send) depending on +`acl.oidc.groups.encoded.in.token`. The principal needs the `HTTP` endpoint permission and, for the switch, `SWITCH ROLE`. With access control disabled no credentials are needed. TLS for this port is configured with the `http.min.tls.*` settings on the [TLS](/docs/configuration/tls/) page. diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 168f459ab1..085b88423a 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -490,10 +490,12 @@ send the device code or the user's Identity Provider password to QuestDB. Before using the flow: -1. Enable the device authorization grant for the public client registered with - the Identity Provider. The discovery examples below use the client ID in - `acl.oidc.client.id`; configure the client explicitly when an application - has its own registration. +1. Register the application as a public client with the Identity Provider and + enable the device authorization grant on it. Each application which + integrates via OIDC should have its own Client Id, so that it can be told + apart from QuestDB in the provider's audit log and given its own policies. + Pass that Client Id to the client explicitly, as described under + [Explicit configuration and endpoint pinning](#explicit-configuration-and-endpoint-pinning). 2. Make the device authorization endpoint available to clients. For the zero-configuration examples below, `acl.oidc.configuration.url` must resolve a provider document containing `device_authorization_endpoint`, or the @@ -524,6 +526,11 @@ Each example discovers the OIDC client ID, scope, token-selection mode, and provider endpoints from QuestDB's [settings endpoint](#settings-endpoint), then signs in before opening the database connection. +Discovering the client ID means reusing QuestDB's own registration, which keeps +the examples short but gives the application no separate identity at the +provider. That is fine for evaluation; in production register the application +and set its Client Id explicitly, as described below. + The discovery URL must use `https`: reaching QuestDB over plaintext would let an attacker replace the advertised Identity Provider endpoints and redirect the device code and the refresh token, so the clients reject it rather than warn. diff --git a/documentation/security/rbac.md b/documentation/security/rbac.md index 5b10f3c266..77c14eaaf7 100644 --- a/documentation/security/rbac.md +++ b/documentation/security/rbac.md @@ -314,13 +314,20 @@ dropped, all members lose the permissions they inherited from that group. width={745} /> -QuestDB supports three authentication methods: - -| Method | Use case | Endpoints | -| ------------------ | ------------------------ | ------------------------- | -| **Password** | Interactive users | REST API, PostgreSQL Wire | -| **JWK Token** | ILP ingestion | InfluxDB Line Protocol | -| **REST API Token** | Programmatic REST access | REST API | +QuestDB supports four authentication methods: + +| Method | Use case | Endpoints | +| --------------------- | --------------------------- | ------------------------------ | +| **Password** | Interactive users | REST API, PostgreSQL Wire | +| **JWK Token** | ILP ingestion | InfluxDB Line Protocol | +| **REST API Token** | Programmatic REST access | REST API | +| **OIDC bearer token** | SSO users, external clients | REST API, PostgreSQL Wire, QWP | + +The first three are QuestDB's own credentials, created with the statements +below. An OIDC bearer token is issued by an external Identity Provider instead, +and the user's group memberships come from the token or the provider's user +info endpoint; see [OpenID Connect](/docs/security/oidc/) and +[which token to send](/docs/security/oidc/#which-token-to-send). Users can have multiple authentication methods enabled simultaneously: From 81131f9d213f40c7fae151796617d77e5c8f7746 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Thu, 3 Sep 2026 01:11:27 +0100 Subject: [PATCH 37/38] Give the OIDC device flow its own page and single-source its examples The device flow was 350 lines with no sidebar path: reachable only by landing on the OIDC guide and scrolling to entry 16 of 32. It is now security/oidc-device-flow.mdx with its own sidebar entry, and the guide keeps a short section at #device-authorization-flow so external links still resolve. Every internal link points at the new page directly rather than through that stub. Extracting it also settles the ordering: the flow no longer sits 250 lines above the "Interactive clients" taxonomy that motivates it, and the Jupyter and CLI sections simply link out. The five examples move to partials under documentation/partials/ and are imported by both the new page and the client page for that language. They were duplicated before, and the C copy had already drifted: the guide used goto fail and printed the error, the client page used goto done and discarded it. One copy now, so they cannot diverge again. Each example also does some work. All five stopped at connect(), so the rotating token was never seen carrying a request; they now write a row to trades and flush, using the same schema and idiom as each page's quick start. The C++ example gains the qwp_reader.hpp include. Its comment advertised pool.borrow_reader(), which does not compile against the sender header alone because the return type is incomplete there. Which token to send is promoted to a top-level section. Nine pages link to it as the canonical answer for the Authorization header, and it sat as an h3 under Settings endpoint, where a reader arriving from a client page landed mid-way through a JSON response contract. The two paragraphs that really do belong to the settings endpoint - the note on reading acl.oidc.enabled first, and the http.context.settings path - move back above it. Finally, the OIDC subsection on the Python and Rust client pages was placed before the parent section's own content, so it swallowed the general TLS root-store tables and, in Python, the WebSocket-upgrade auth prose. Both now sit where the C and C++ page already put theirs. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY --- documentation/changelog.mdx | 2 +- documentation/configuration/oidc.md | 4 +- documentation/connect/clients/c-and-cpp.md | 73 +--- .../connect/clients/connect-string.md | 2 +- documentation/connect/clients/dotnet.md | 2 +- documentation/connect/clients/go.md | 2 +- documentation/connect/clients/java.md | 21 +- documentation/connect/clients/python.md | 65 ++- documentation/connect/clients/rust.md | 51 +-- .../partials/_oidc.device-flow.c.partial.mdx | 70 +++ .../_oidc.device-flow.cpp.partial.mdx | 33 ++ .../_oidc.device-flow.java.partial.mdx | 24 ++ .../_oidc.device-flow.python.partial.mdx | 22 + .../_oidc.device-flow.rust.partial.mdx | 37 ++ documentation/security/oidc-device-flow.mdx | 246 +++++++++++ documentation/security/oidc.mdx | 408 ++---------------- documentation/sidebars.js | 5 + 17 files changed, 530 insertions(+), 537 deletions(-) create mode 100644 documentation/partials/_oidc.device-flow.c.partial.mdx create mode 100644 documentation/partials/_oidc.device-flow.cpp.partial.mdx create mode 100644 documentation/partials/_oidc.device-flow.java.partial.mdx create mode 100644 documentation/partials/_oidc.device-flow.python.partial.mdx create mode 100644 documentation/partials/_oidc.device-flow.rust.partial.mdx create mode 100644 documentation/security/oidc-device-flow.mdx diff --git a/documentation/changelog.mdx b/documentation/changelog.mdx index 433d854b70..4502d0dc19 100644 --- a/documentation/changelog.mdx +++ b/documentation/changelog.mdx @@ -42,7 +42,7 @@ This page tracks significant updates to the QuestDB documentation. - [ALTER TABLE SET FORMAT](/docs/query/sql/alter-table-set-format/) - New reference page for switching a table's partition storage format between `NATIVE` and `PARQUET` - [QWP configuration](/docs/configuration/qwp/) - Server-side settings for the QWP ingestion (`/write/v4`) and query (`/read/v1`) endpoints - [Check timestamp order](/docs/cookbook/sql/time-series/check-timestamp-order/) and [Check column sort order](/docs/cookbook/sql/advanced/check-column-sort-order/) - Two cookbook recipes that detect unsorted data with `lag()`: whether a table, CSV import or external Parquet file is ordered by its timestamp, and whether one column is sorted with respect to another -- [OIDC Device Authorization Flow](/docs/security/oidc/#device-authorization-flow) - Interactive OIDC sign-in for headless and remote applications, including configuration, token lifecycle, a protocol walkthrough, and examples for the Java, Python, Rust, C, and C++ clients +- [OIDC Device Authorization Flow](/docs/security/oidc-device-flow/) - Interactive OIDC sign-in for headless and remote applications, including configuration, token lifecycle, a protocol walkthrough, and examples for the Java, Python, Rust, C, and C++ clients - [OIDC settings endpoint](/docs/security/oidc/#settings-endpoint) - How a client reads QuestDB's OIDC configuration from `/settings` to discover the authorization, token, and device authorization endpoints, and whether PKCE and the `state` parameter are required - [Kubernetes Operator](/docs/enterprise-kubernetes-operator/) - New manual for running QuestDB Enterprise clusters on Kubernetes, covering [installation](/docs/enterprise-kubernetes-operator/installation/), getting started on [AWS](/docs/enterprise-kubernetes-operator/getting-started/aws/) and [Azure](/docs/enterprise-kubernetes-operator/getting-started/azure/), [configuration](/docs/enterprise-kubernetes-operator/configuration/), [day-to-day operations](/docs/enterprise-kubernetes-operator/operations/operator/), [high availability](/docs/enterprise-kubernetes-operator/high-availability/), [known limitations](/docs/enterprise-kubernetes-operator/known-limitations/), and the full [API reference](/docs/enterprise-kubernetes-operator/reference/api/) - [ALTER TABLE SUSPEND WAL](/docs/query/sql/alter-table-suspend-wal/) - New reference page for deliberately stopping the WAL apply job, covering the quiescent-table use case that `REBASE WAL` requires, and the trap that writes to a suspended table succeed while staying invisible to queries until `RESUME WAL` diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index c0b6e30085..08cc01049d 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -159,7 +159,7 @@ QuestDB uses the scopes in the requests it makes itself, in the [settings endpoint](/docs/security/oidc/#settings-endpoint) for clients which run the flow themselves. -For the [Device Authorization Flow](/docs/security/oidc/#device-authorization-flow), +For the [Device Authorization Flow](/docs/security/oidc-device-flow/), add `offline_access` when the provider requires that scope before issuing a refresh token. Without a refresh token, a client must ask the user to sign in again after the current token expires. @@ -253,7 +253,7 @@ has no default, and QuestDB never calls it. QuestDB resolves the endpoint and publishes it on the [settings endpoint](/docs/security/oidc/#settings-endpoint), for clients which implement the -[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow) +[Device Authorization Flow](/docs/security/oidc-device-flow/) themselves. The official Java, Python, Rust, C, and C++ clients can discover and use the published endpoint. diff --git a/documentation/connect/clients/c-and-cpp.md b/documentation/connect/clients/c-and-cpp.md index fc53123385..ea98fcd735 100644 --- a/documentation/connect/clients/c-and-cpp.md +++ b/documentation/connect/clients/c-and-cpp.md @@ -7,6 +7,8 @@ description: "QuestDB C and C++ client: the questdb_db / questdb::pool connectio import Tabs from "@theme/Tabs" import TabItem from "@theme/TabItem" +import OidcDeviceFlowCpp from "../../partials/_oidc.device-flow.cpp.partial.mdx"; +import OidcDeviceFlowC from "../../partials/_oidc.device-flow.c.partial.mdx"; The C and C++ clients ingest and query over [QWP](/docs/connect/wire-protocols/qwp-ingress-websocket/), a columnar binary @@ -328,7 +330,7 @@ Handle it there, not at `connect` (see ### OIDC device flow (Enterprise) The C and C++ APIs sign in an interactive user with the -[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow). +[Device Authorization Flow](/docs/security/oidc-device-flow/). They discover the provider endpoints, client ID, scope, and the token QuestDB expects from the server's public `/settings` endpoint. Attach the resulting auth object to the pool so sender and reader connections share its rotating token: @@ -336,75 +338,12 @@ object to the pool so sender and reader connections share its rotating token: -```cpp -#include -#include -#include - -int main() { - auto auth = questdb::oidc::builder::from_questdb( - "https://questdb.example.com:9000") - .event_handler([](const questdb::oidc::event_view& event) { - if (event.kind() == questdb::oidc::event_kind::prompt) - std::cerr << "Open " << event.verification_uri() - << " and enter " << event.user_code() << '\n'; - }) - .build(); - - auth.sign_in(); // the only call which may prompt or open a browser - questdb::pool pool{"wss::addr=questdb.example.com:9000;", auth}; -} -``` + -```c -#include -#include -#include -#include - -static void show_prompt(void *data, const questdb_oidc_event *event) { - (void)data; - if (event->kind == QUESTDB_OIDC_EVENT_PROMPT) - fprintf(stderr, "Open %.*s and enter %.*s\n", - (int)event->verification_uri_len, event->verification_uri, - (int)event->user_code_len, event->user_code); -} - -int main(void) { - questdb_error *error = NULL; - questdb_oidc_builder *builder = NULL; - questdb_oidc_auth *auth = NULL; - questdb_db *db = NULL; - int status = 1; - const char *url = "https://questdb.example.com:9000"; - builder = questdb_oidc_builder_from_questdb(url, strlen(url), &error); - if (!builder || !questdb_oidc_builder_event_handler( - builder, show_prompt, NULL, NULL, &error)) - goto done; - - auth = questdb_oidc_builder_build(builder, &error); - if (!auth || !questdb_oidc_auth_sign_in(auth, &error)) - goto done; - - questdb_db_connect_options options; - questdb_db_connect_options_init(&options, sizeof options); - options.oidc_auth = auth; - const char *conf = "wss::addr=questdb.example.com:9000;"; - db = questdb_db_connect_ex(conf, strlen(conf), &options, &error); - if (db) - status = 0; - -done: - questdb_db_close(db); - questdb_oidc_auth_free(auth); - questdb_oidc_builder_free(builder); - questdb_error_free(error); - return status; -} -``` + @@ -429,7 +368,7 @@ names are prefixed with `questdb_oidc_builder_`. Tokens stay in memory until `.default_file_token_store()` in C++, or `questdb_oidc_builder_default_file_token_store()` in C, writes a long-lived refresh token to disk as plaintext. See the [complete client -examples](/docs/security/oidc/#official-client-examples) for error handling, the +examples](/docs/security/oidc-device-flow/#official-client-examples) for error handling, the sign-in prompt, and the store's location and permissions. ### Other authentication limitations diff --git a/documentation/connect/clients/connect-string.md b/documentation/connect/clients/connect-string.md index 4283cee237..9e0d5babe6 100644 --- a/documentation/connect/clients/connect-string.md +++ b/documentation/connect/clients/connect-string.md @@ -223,7 +223,7 @@ WebSocket upgrade request. - Device-flow OIDC credentials are **not** connect-string keys. The Java, Python, Rust, C and C++ clients take an auth object alongside the connect string, which rotates the token on every reconnect; see - [OIDC device flow](/docs/security/oidc/#device-authorization-flow). A + [OIDC device flow](/docs/security/oidc-device-flow/). A static `token=` does not rotate, so a reconnect after expiry keeps sending the stale value. - `auth_timeout_ms` — per-host upper bound on the upgrade response read. diff --git a/documentation/connect/clients/dotnet.md b/documentation/connect/clients/dotnet.md index c36dada8bf..76df3b4781 100644 --- a/documentation/connect/clients/dotnet.md +++ b/documentation/connect/clients/dotnet.md @@ -228,7 +228,7 @@ that purpose; left unset, it inherits `auth_timeout_ms`. | Path | Status | Workaround | |---|---|---| -| OIDC token acquisition or in-band refresh | Not supported by this client. It does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC — see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...` above, and rebuild the sender / query client when the token nears expiry. To acquire one, run the [Device Authorization Flow](/docs/security/oidc/#device-authorization-flow) with an OAuth2 library such as MSAL.NET; QuestDB publishes the device authorization endpoint on the same settings response. | +| OIDC token acquisition or in-band refresh | Not supported by this client. It does not negotiate with an identity provider and has no callback to refresh a token mid-session. | QuestDB itself supports OIDC — see [OpenID Connect](/docs/security/oidc/). Acquire a token out-of-band from your IdP, using QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint) to discover the provider's authorization and token endpoints and [which token to send](/docs/security/oidc/#which-token-to-send), pass it via `token=...` above, and rebuild the sender / query client when the token nears expiry. To acquire one, run the [Device Authorization Flow](/docs/security/oidc-device-flow/) with an OAuth2 library such as MSAL.NET; QuestDB publishes the device authorization endpoint on the same settings response. | | Mutual TLS (client certificates) | Not supported. The QuestDB server does not negotiate client certificates regardless of client. | Use bearer-token auth over `wss://`. See the connect-string reference for the canonical statement. | | Token rotation mid-session | Not supported. Credentials are presented once during the WebSocket upgrade and are not re-sent. | On token expiry, `await sender.DisposeAsync()` and build a fresh sender with the new token. The same applies to `QueryClient`. | diff --git a/documentation/connect/clients/go.md b/documentation/connect/clients/go.md index 086781b576..b66b2748c5 100644 --- a/documentation/connect/clients/go.md +++ b/documentation/connect/clients/go.md @@ -370,7 +370,7 @@ token, or the ID token when QuestDB reads group memberships from the token. This client does not run an OIDC flow, so acquire the token with an OAuth2 library: `golang.org/x/oauth2` implements the device grant, and QuestDB publishes the device authorization endpoint on the same settings response. The -[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow) +[Device Authorization Flow](/docs/security/oidc-device-flow/) describes the protocol, including the polling interval, `slow_down`, and the device code's expiry. When the token expires or is rotated, construct a new handle with the new token. diff --git a/documentation/connect/clients/java.md b/documentation/connect/clients/java.md index cc963c1dd2..dfb9932c92 100644 --- a/documentation/connect/clients/java.md +++ b/documentation/connect/clients/java.md @@ -12,6 +12,7 @@ import TabItem from "@theme/TabItem" import SfDedupWarning from "../../partials/_sf-dedup-warning.partial.mdx" import CodeBlock from "@theme/CodeBlock" +import OidcDeviceFlowExample from "../../partials/_oidc.device-flow.java.partial.mdx"; :::note @@ -400,27 +401,13 @@ both the ingress and egress WebSocket upgrades. It is mutually exclusive with ### OIDC device flow (Enterprise) `OidcDeviceAuth` signs in an interactive user with the -[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow). +[Device Authorization Flow](/docs/security/oidc-device-flow/). It discovers the provider endpoints, client ID, scope, and the token QuestDB expects from the server's public `/settings` endpoint. The user can approve the sign-in from a browser on any device, so this also works from a container or remote notebook kernel: -```java -import io.questdb.client.QuestDB; -import io.questdb.client.cutlass.auth.OidcDeviceAuth; - -try (OidcDeviceAuth auth = OidcDeviceAuth.fromQuestDB( - "https://questdb.example.com:9000")) { - auth.signIn(); // the only call which may prompt or open a browser - - try (QuestDB db = QuestDB.connect( - "wss::addr=questdb.example.com:9000;", - auth::getToken)) { - // Ingestion and query connections share the rotating token provider. - } -} -``` + Pass `auth::getToken` as a provider instead of putting the current token in the connect string. Every new connection and reconnect then receives the cached or @@ -443,7 +430,7 @@ with `OidcDeviceAuth.builder()`. Tokens stay in memory until a store is attached with `DiscoveryOptions.tokenStore(FileTokenStore.atDefaultLocation())`, which writes a long-lived refresh token to disk as plaintext. See the [OIDC guide's client -examples](/docs/security/oidc/#official-client-examples) for the sign-in prompt, +examples](/docs/security/oidc-device-flow/#official-client-examples) for the sign-in prompt, explicit configuration, and the token store's location and permissions. ### HTTP basic auth diff --git a/documentation/connect/clients/python.md b/documentation/connect/clients/python.md index 3b78289a15..e5b0358fe5 100644 --- a/documentation/connect/clients/python.md +++ b/documentation/connect/clients/python.md @@ -5,6 +5,8 @@ sidebar_label: Python description: "Use the QuestDB Python client's QuestDB pool for row, DataFrame, and Arrow ingestion plus SQL queries over QWP." --- +import OidcDeviceFlowExample from "../../partials/_oidc.device-flow.python.partial.mdx"; + The QuestDB Python client uses one `QuestDB` handle for ingestion and SQL queries over [QWP](/docs/connect/wire-protocols/qwp-ingress-websocket/). Lease a short-lived sender for each unit of row-building work, bulk-load DataFrames through the handle, and run SQL with `query()`. @@ -142,28 +144,37 @@ token = questdb.connect( ) ``` +Authentication happens during the WebSocket upgrade, before any data frames +are exchanged. Bad credentials raise `QuestDBErrorCode.AuthError` from the +first operation that needs the connection, not from `connect()`. Queries and +`dataframe()` calls raise it directly. Row senders connect in the +background, so a fire-and-forget `flush()` can return before the upgrade +fails; the error then surfaces from `flush(wait=True)`, `wait()`, or the +next call on the lease — and immediately through the +[connection listener](#failover-and-errors) as an `AuthFailed` event. + +With `wss`, the default combines the bundled `webpki` root store with the +operating-system certificate store. Override it with configuration keys: + +| Setting | Meaning | +| --- | --- | +| `tls_ca=os_roots` | Use only the operating-system certificate store. | +| `tls_ca=webpki_roots` | Use only the bundled `webpki` root store. | +| `tls_roots=/path/to/ca.pem` | Use a private CA bundle. | +| `tls_verify=unsafe_off` | Rejected: certificate verification cannot be disabled in released wheels. | + +See the [connect string reference](/docs/connect/clients/connect-string/) for +the full grammar. + ### OIDC device flow (Enterprise) `OidcDeviceAuth` signs in an interactive user with the -[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow). +[Device Authorization Flow](/docs/security/oidc-device-flow/). It discovers the provider endpoints, client ID, scope, and the token QuestDB expects from the server's public `/settings` endpoint. The default prompt works in terminals and remote Jupyter kernels: -```python -import questdb -from questdb.auth import OidcDeviceAuth - -with OidcDeviceAuth.from_questdb( - "https://questdb.example.com:9000") as auth: - auth.sign_in() # the only call which may prompt or open a browser - - with questdb.connect( - "wss::addr=questdb.example.com:9000;", - oidc_auth=auth) as db: - # sender(), dataframe(), and query() share the rotating token provider. - pass -``` + `oidc_auth=auth` keeps shared ownership of the provider and obtains the cached or silently refreshed token for every connection and reconnect. It is mutually @@ -184,31 +195,9 @@ Otherwise pass `issuer=...`, or set `client_id=`, `scope=`, `audience=`, Tokens stay in memory until a store is passed as `token_store=FileTokenStore.at_default_location()`, which writes a long-lived refresh token to disk as plaintext. See the [OIDC client -examples](/docs/security/oidc/#official-client-examples) for the sign-in prompt, +examples](/docs/security/oidc-device-flow/#official-client-examples) for the sign-in prompt, explicit configuration, and the store's location and permissions. -Authentication happens during the WebSocket upgrade, before any data frames -are exchanged. Bad credentials raise `QuestDBErrorCode.AuthError` from the -first operation that needs the connection, not from `connect()`. Queries and -`dataframe()` calls raise it directly. Row senders connect in the -background, so a fire-and-forget `flush()` can return before the upgrade -fails; the error then surfaces from `flush(wait=True)`, `wait()`, or the -next call on the lease — and immediately through the -[connection listener](#failover-and-errors) as an `AuthFailed` event. - -With `wss`, the default combines the bundled `webpki` root store with the -operating-system certificate store. Override it with configuration keys: - -| Setting | Meaning | -| --- | --- | -| `tls_ca=os_roots` | Use only the operating-system certificate store. | -| `tls_ca=webpki_roots` | Use only the bundled `webpki` root store. | -| `tls_roots=/path/to/ca.pem` | Use a private CA bundle. | -| `tls_verify=unsafe_off` | Rejected: certificate verification cannot be disabled in released wheels. | - -See the [connect string reference](/docs/connect/clients/connect-string/) for -the full grammar. - ### Other authentication limitations - Mutual TLS client certificates are not supported because the QuestDB server diff --git a/documentation/connect/clients/rust.md b/documentation/connect/clients/rust.md index 63823bf02e..fa467b4267 100644 --- a/documentation/connect/clients/rust.md +++ b/documentation/connect/clients/rust.md @@ -5,6 +5,8 @@ sidebar_label: Rust description: "Use the QuestDB Rust connection pool for Buffer, Chunk, Arrow, and Polars ingestion plus streaming SQL queries over QWP." --- +import OidcDeviceFlowExample from "../../partials/_oidc.device-flow.rust.partial.mdx"; + The QuestDB Rust client uses a thread-safe `QuestDb` pool for ingestion and SQL queries over [QWP](/docs/connect/wire-protocols/qwp-ingress-websocket/). Borrow a short-lived writer or reader for each unit of work, then let `Drop` return its @@ -152,10 +154,21 @@ let token = QuestDb::connect( )?; ``` +With the default crate features, the TLS root set is `webpki_roots`. Other +choices have feature requirements: + +| Setting | Requirement | +| --- | --- | +| `tls_ca=os_roots` | Enable `tls-native-certs`. | +| `tls_ca=webpki_and_os_roots` | Enable both `tls-webpki-certs` and `tls-native-certs`. | +| `tls_roots=/path/to/roots.pem` | Uses the supplied PEM bundle and implies `tls_ca=pem_file`. | +| `tls_roots_password=...` | Unlocks a JKS or PKCS#12 store named by `tls_roots`. | +| `tls_verify=unsafe_off` | Enable `insecure-skip-verify`; use only in controlled tests. | + ### OIDC device flow (Enterprise) Enable the `oidc` feature to sign in an interactive user with the -[Device Authorization Flow](/docs/security/oidc/#device-authorization-flow): +[Device Authorization Flow](/docs/security/oidc-device-flow/): ```toml title="Cargo.toml" [dependencies] @@ -165,28 +178,7 @@ questdb-rs = { version = "7", features = ["oidc"] } `OidcDeviceAuth::from_questdb` discovers the provider endpoints, client ID, scope, and the token QuestDB expects from the public `/settings` endpoint: -```rust -use std::sync::Arc; -use questdb::{oidc::OidcDeviceAuth, QuestDb}; - -fn main() -> questdb::Result<()> { - let auth = Arc::new( - OidcDeviceAuth::from_questdb("https://questdb.example.com:9000") - .build()?, - ); - auth.sign_in()?; // the only call which may prompt or open a browser - - let db = QuestDb::connect_with_token_provider( - "wss::addr=questdb.example.com:9000;", - { - let auth = Arc::clone(&auth); - move || auth.token() - }, - )?; - // Sender and reader connections share the rotating token provider. - Ok(()) -} -``` + Pass the closure as a provider instead of putting the current token in the connect string. Every new connection and reconnect then receives the cached or @@ -209,20 +201,9 @@ yourself. Tokens stay in memory until `.token_store(FileTokenStore::at_default_location()?)` is added, which writes a long-lived refresh token to disk as plaintext. See the [OIDC client -examples](/docs/security/oidc/#official-client-examples) for the sign-in prompt, +examples](/docs/security/oidc-device-flow/#official-client-examples) for the sign-in prompt, explicit configuration, and the store's location and permissions. -With the default crate features, the TLS root set is `webpki_roots`. Other -choices have feature requirements: - -| Setting | Requirement | -| --- | --- | -| `tls_ca=os_roots` | Enable `tls-native-certs`. | -| `tls_ca=webpki_and_os_roots` | Enable both `tls-webpki-certs` and `tls-native-certs`. | -| `tls_roots=/path/to/roots.pem` | Uses the supplied PEM bundle and implies `tls_ca=pem_file`. | -| `tls_roots_password=...` | Unlocks a JKS or PKCS#12 store named by `tls_roots`. | -| `tls_verify=unsafe_off` | Enable `insecure-skip-verify`; use only in controlled tests. | - ## The pool `QuestDb` owns reusable QWP/WebSocket connections. Create one pool per diff --git a/documentation/partials/_oidc.device-flow.c.partial.mdx b/documentation/partials/_oidc.device-flow.c.partial.mdx new file mode 100644 index 0000000000..de8dfd2381 --- /dev/null +++ b/documentation/partials/_oidc.device-flow.c.partial.mdx @@ -0,0 +1,70 @@ +```c +#include +#include // questdb_db_borrow_sender + line_sender_buffer +#include +#include +#include + +static void show_prompt(void *data, const questdb_oidc_event *event) { + (void)data; + if (event->kind == QUESTDB_OIDC_EVENT_PROMPT) + fprintf(stderr, "Open %.*s and enter %.*s\n", + (int)event->verification_uri_len, event->verification_uri, + (int)event->user_code_len, event->user_code); +} + +int main(void) { + questdb_error *error = NULL; + questdb_oidc_builder *builder = NULL; + questdb_oidc_auth *auth = NULL; + questdb_db *db = NULL; + qwp_sender *sender = NULL; + line_sender_buffer *buffer = NULL; + int status = 1; + + const char *url = "https://questdb.example.com:9000"; + builder = questdb_oidc_builder_from_questdb(url, strlen(url), &error); + if (!builder || !questdb_oidc_builder_event_handler( + builder, show_prompt, NULL, NULL, &error)) + goto done; + + auth = questdb_oidc_builder_build(builder, &error); + if (!auth || !questdb_oidc_auth_sign_in(auth, &error)) + goto done; + + questdb_db_connect_options options; + questdb_db_connect_options_init(&options, sizeof options); + options.oidc_auth = auth; + + const char *conf = "wss::addr=questdb.example.com:9000;"; + db = questdb_db_connect_ex(conf, strlen(conf), &options, &error); + if (!db) goto done; + + /* Sender and reader borrows use the rotating token. */ + sender = questdb_db_borrow_sender(db, &error); + if (!sender) goto done; + buffer = questdb_db_new_buffer(db, &error); + if (!buffer) goto done; + if (!line_sender_buffer_table(buffer, QDB_TABLE_NAME_LITERAL("trades"), &error)) goto done; + if (!line_sender_buffer_symbol(buffer, QDB_COLUMN_NAME_LITERAL("symbol"), + QDB_UTF8_LITERAL("ETH-USD"), &error)) goto done; + if (!line_sender_buffer_column_f64(buffer, QDB_COLUMN_NAME_LITERAL("price"), 2615.54, &error)) goto done; + if (!line_sender_buffer_at_nanos(buffer, line_sender_now_nanos(), &error)) goto done; + if (!qwp_sender_flush_buffer_and_wait(sender, buffer, qwpws_ack_level_ok, &error)) goto done; + status = 0; + +done: + if (error) { + size_t len = 0; + const char *message = questdb_error_msg(error, &len); + fprintf(stderr, "OIDC device flow failed: %.*s\n", (int)len, message); + questdb_error_free(error); + } + line_sender_buffer_free(buffer); + if (sender) questdb_db_return_sender(db, sender); + questdb_db_close(db); + questdb_oidc_auth_free(auth); + questdb_oidc_builder_free(builder); + return status; +} +``` diff --git a/documentation/partials/_oidc.device-flow.cpp.partial.mdx b/documentation/partials/_oidc.device-flow.cpp.partial.mdx new file mode 100644 index 0000000000..cdc84f0d69 --- /dev/null +++ b/documentation/partials/_oidc.device-flow.cpp.partial.mdx @@ -0,0 +1,33 @@ +```cpp +#include // pool::borrow_sender +#include // pool::borrow_reader +#include +#include + +using namespace questdb::ingress::literals; + +int main() { + auto auth = questdb::oidc::builder::from_questdb( + "https://questdb.example.com:9000") + .event_handler([](const questdb::oidc::event_view& event) { + if (event.kind() == questdb::oidc::event_kind::prompt) + std::cerr << "Open " << event.verification_uri() + << " and enter " << event.user_code() << '\n'; + }) + .build(); + + auth.sign_in(); // the only call which may prompt or open a browser + questdb::pool pool{"wss::addr=questdb.example.com:9000;", auth}; + + auto sender = pool.borrow_sender(); + auto buffer = sender.new_buffer(); + buffer.table("trades"_tn) + .symbol("symbol"_cn, "ETH-USD"_utf8) + .symbol("side"_cn, "sell"_utf8) + .column("price"_cn, 2615.54) + .column("amount"_cn, 0.00044) + .at(questdb::ingress::timestamp_nanos::now()); + sender.flush_and_wait(buffer); + // pool.borrow_reader() uses the same rotating token. +} +``` diff --git a/documentation/partials/_oidc.device-flow.java.partial.mdx b/documentation/partials/_oidc.device-flow.java.partial.mdx new file mode 100644 index 0000000000..361ad24f43 --- /dev/null +++ b/documentation/partials/_oidc.device-flow.java.partial.mdx @@ -0,0 +1,24 @@ +```java +import io.questdb.client.QuestDB; +import io.questdb.client.Sender; +import io.questdb.client.cutlass.auth.OidcDeviceAuth; + +try (OidcDeviceAuth auth = OidcDeviceAuth.fromQuestDB( + "https://questdb.example.com:9000")) { + auth.signIn(); // the only call which may prompt or open a browser + + try (QuestDB db = QuestDB.connect( + "wss::addr=questdb.example.com:9000;", + auth::getToken)) { + try (Sender sender = db.borrowSender()) { + sender.table("trades") + .symbol("symbol", "ETH-USD") + .symbol("side", "sell") + .doubleColumn("price", 2615.54) + .doubleColumn("amount", 0.00044) + .atNow(); + } + // db.borrowQuery() uses the same rotating token. + } +} +``` diff --git a/documentation/partials/_oidc.device-flow.python.partial.mdx b/documentation/partials/_oidc.device-flow.python.partial.mdx new file mode 100644 index 0000000000..dc06df19bb --- /dev/null +++ b/documentation/partials/_oidc.device-flow.python.partial.mdx @@ -0,0 +1,22 @@ +```python +import questdb +from questdb import TimestampNanos +from questdb.auth import OidcDeviceAuth + +with OidcDeviceAuth.from_questdb( + "https://questdb.example.com:9000") as auth: + auth.sign_in() # the only call which may prompt or open a browser + + with questdb.connect( + "wss::addr=questdb.example.com:9000;", + oidc_auth=auth) as db: + with db.sender() as sender: + sender.row( + "trades", + symbols={"symbol": "ETH-USD", "side": "sell"}, + columns={"price": 2615.54, "amount": 0.00044}, + at=TimestampNanos.now(), + ) + sender.flush(wait=True) + # db.dataframe() and db.query() use the same rotating token. +``` diff --git a/documentation/partials/_oidc.device-flow.rust.partial.mdx b/documentation/partials/_oidc.device-flow.rust.partial.mdx new file mode 100644 index 0000000000..b333dfc372 --- /dev/null +++ b/documentation/partials/_oidc.device-flow.rust.partial.mdx @@ -0,0 +1,37 @@ +```rust +use std::sync::Arc; +use questdb::{ + ingress::{AckLevel, TimestampNanos}, + oidc::OidcDeviceAuth, + QuestDb, +}; + +fn main() -> questdb::Result<()> { + let auth = Arc::new( + OidcDeviceAuth::from_questdb("https://questdb.example.com:9000") + .build()?, + ); + auth.sign_in()?; // the only call which may prompt or open a browser + + let db = QuestDb::connect_with_token_provider( + "wss::addr=questdb.example.com:9000;", + { + let auth = Arc::clone(&auth); + move || auth.token() + }, + )?; + + let mut sender = db.borrow_sender()?; + let mut buffer = sender.new_buffer(); + buffer + .table("trades")? + .symbol("symbol", "ETH-USD")? + .symbol("side", "sell")? + .column_f64("price", 2615.54)? + .column_f64("amount", 0.00044)? + .at(TimestampNanos::now())?; + sender.flush_buffer_and_wait(&mut buffer, AckLevel::Ok)?; + // db.borrow_reader() uses the same rotating token. + Ok(()) +} +``` diff --git a/documentation/security/oidc-device-flow.mdx b/documentation/security/oidc-device-flow.mdx new file mode 100644 index 0000000000..5b1b4d25d1 --- /dev/null +++ b/documentation/security/oidc-device-flow.mdx @@ -0,0 +1,246 @@ +--- +title: OIDC Device Authorization Flow +description: Sign a user in to QuestDB Enterprise from a command-line application, container, or remote notebook kernel with the OAuth 2.0 device authorization grant, with worked client examples. +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import JavaExample from "../partials/_oidc.device-flow.java.partial.mdx"; +import PythonExample from "../partials/_oidc.device-flow.python.partial.mdx"; +import RustExample from "../partials/_oidc.device-flow.rust.partial.mdx"; +import CppExample from "../partials/_oidc.device-flow.cpp.partial.mdx"; +import CExample from "../partials/_oidc.device-flow.c.partial.mdx"; +import { EnterpriseNote } from "@site/src/components/EnterpriseNote" + + + The Device Authorization Flow signs in an interactive user from a client that + cannot receive a browser redirect. + + +The [OAuth 2.0 Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628) +signs in a person without redirecting a browser back to the client. It is a good +fit for command-line applications, containers, and remote notebook kernels: the +client displays a verification URL and short code, and the user can authorize +from a browser on any laptop or phone. + +QuestDB does not run the device flow. The client communicates directly with the +Identity Provider, then presents the resulting bearer token to QuestDB: + +```mermaid +sequenceDiagram + participant Client as QuestDB client + participant QDB as QuestDB + participant IdP as Identity Provider + participant User + Client->>QDB: GET /settings + QDB-->>Client: client ID, scope, endpoints, token mode + Client->>IdP: Request device and user codes + IdP-->>Client: device_code, user_code, verification URI + Client-->>User: Display URL and code + User->>IdP: Sign in and approve in a browser + loop Until approved or expired + Client->>IdP: Poll token endpoint + IdP-->>Client: authorization_pending / tokens + end + Client->>QDB: Authorization: Bearer selected token +``` + +The client must respect the polling interval, `slow_down` responses, and the +device code's expiry. The official clients handle those protocol details, +choose the access or ID token according to +[`acl.oidc.groups.encoded.in.token`](/docs/security/oidc/#which-token-to-send), cache it in memory, +and refresh it silently when the provider issues a refresh token. They never +send the device code or the user's Identity Provider password to QuestDB. + +## Configure the provider and QuestDB + +Before using the flow: + +1. Register the application as a public client with the Identity Provider and + enable the device authorization grant on it. Each application which + integrates via OIDC should have its own Client Id, so that it can be told + apart from QuestDB in the provider's audit log and given its own policies. + Pass that Client Id to the client explicitly, as described under + [Explicit configuration and endpoint pinning](#explicit-configuration-and-endpoint-pinning). +2. Make the device authorization endpoint available to clients. For the + zero-configuration examples below, `acl.oidc.configuration.url` must resolve + a provider document containing `device_authorization_endpoint`, or the + host-based configuration must set + [`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint). +3. Keep `openid` in `acl.oidc.scope`. Add `offline_access` if your provider + requires that scope before issuing a refresh token to a device-flow client. + +For example, a host-based configuration can add: + +```ini title="server.conf" +acl.oidc.scope=openid offline_access +acl.oidc.device.authorization.endpoint=/as/device_authz.oauth2 +``` + +The exact endpoint path and refresh-token scope are provider-specific. With +configuration-document discovery, do not set the endpoint property: all +individual endpoint settings are ignored in that mode. + +If QuestDB does not publish the device authorization endpoint, pin the provider +with the client's `issuer` option so the client can fetch the provider's +`/.well-known/openid-configuration` document, or configure the client ID and +both endpoints explicitly. + +## Official client examples + +Each example discovers the OIDC client ID, scope, token-selection mode, and +provider endpoints from QuestDB's [settings endpoint](/docs/security/oidc/#settings-endpoint), then +signs in before opening the database connection. + +Discovering the client ID means reusing QuestDB's own registration, which keeps +the examples short but gives the application no separate identity at the +provider. That is fine for evaluation; in production register the application +and set its Client Id explicitly, as described below. + +The discovery URL must use `https`: reaching QuestDB over plaintext would let an +attacker replace the advertised Identity Provider endpoints and redirect the +device code and the refresh token, so the clients reject it rather than warn. +The Rust, C, C++ and Python clients accept loopback `http` for local +development; the Java client does not. To use a plaintext non-loopback host +anyway, opt in with `allowInsecureTransport(true)` (Java), +`.allow_insecure_transport(true)` (Rust), +`questdb_oidc_builder_allow_insecure_transport()` (C), +`allow_insecure_transport()` (C++) or `insecure=True` (Python). + + + + + + + + + + + + + + +Enable the `oidc` crate feature: + +```toml title="Cargo.toml" +[dependencies] +questdb-rs = { version = "7", features = ["oidc"] } +``` + + + + + + + + + + + + + + + + +The authentication object owns the token state. Pass it as a rotating provider, +as shown above, instead of copying the current token into the connection string; +otherwise a reconnect after token expiry will keep sending the stale value. + +Device flow always requires a person to approve the sign-in. For unattended +services, scheduled jobs, or CI, use a service-account token or a provider flow +intended for machine identities instead. + +### The sign-in prompt + +Signing in renders the verification URL and the user code, then blocks until the +user approves or the device code expires. Where that text goes, and whether the +call runs at all without a terminal, differs by client: + +| Client | Default prompt | Without a terminal | +| --- | --- | --- | +| Java | prints to `System.out`, then tries to open a browser | runs anyway; the client performs no terminal check | +| Python | prints to stderr, or to the frontend inside an IPython kernel | fails, unless the kernel accepts stdin | +| Rust, C, C++ | prints to stderr | fails with an interaction-required error | + +Redirecting output therefore hides the code in Java, while a container with no +TTY fails outright in the other four. Override the default to render the +challenge elsewhere, or to declare the process interactive anyway: + +| Client | Custom prompt | Force interactive | +| --- | --- | --- | +| Java | `prompt(...)`, or `DeviceCodePrompt.SYSTEM_OUT` to skip the browser launch | not applicable | +| Python | `renderer=` | `interactive=True` | +| Rust | `.renderer(...)` | `.interactive(true)` | +| C | `questdb_oidc_builder_event_handler()` | `questdb_oidc_builder_interactive()` | +| C++ | `.event_handler(...)` | `.interactive(true)` | + +The prompt's text fields are display-safe. Where a client opens the URL or makes +it clickable, use the separately vetted browser target instead: `browser_target` +on the C event struct, `event_view::browser_target()` in C++. + +### Explicit configuration and endpoint pinning + +Discovery supplies the client ID, scope, audience, token-selection mode, and +endpoints. Set any of them explicitly when the application has its own +registration with the provider, or when QuestDB does not publish what the client +needs: + +| Setting | Set it when | +| --- | --- | +| `client_id` | the application has its own registration, which is the recommended setup | +| `scope` | the provider requires `offline_access` before it issues a refresh token | +| `audience` | the provider requires one, or QuestDB validates `aud` against a non-default [`acl.oidc.audience`](/docs/configuration/oidc/#acloidcaudience). QuestDB does not publish that key, so discovery cannot supply it | +| `issuer` | QuestDB publishes no device authorization endpoint, so the client reads the provider's `/.well-known/openid-configuration` instead | +| `token_endpoint`, `device_authorization_endpoint` | pinning both endpoints directly rather than through an issuer | +| `groups_in_token` | overriding which of the two tokens the client selects | + +Java exposes `issuer`, `prompt`, `tokenStore`, `tlsConfig` and +`allowInsecureTransport` on `OidcDeviceAuth.DiscoveryOptions`, and the full set +on `OidcDeviceAuth.builder()`, in camel case. Python passes them as keyword +arguments to `from_questdb`. Rust and C++ are builder methods in snake case, and +C prefixes the same names with `questdb_oidc_builder_`. + +### Token persistence + +Tokens stay in memory unless a file token store is enabled. The store writes the +access, ID and long-lived refresh tokens as unencrypted JSON, so turn it on only +where that at-rest exposure is acceptable: + +| Client | Default location | A directory you choose | +| --- | --- | --- | +| Java | `FileTokenStore.atDefaultLocation()` | `FileTokenStore.at(path)` | +| Python | `FileTokenStore.at_default_location()` | `FileTokenStore.at(path)` | +| Rust | `FileTokenStore::at_default_location()?` | `FileTokenStore::at(path)` | +| C | `questdb_oidc_builder_default_file_token_store()` | `questdb_oidc_builder_file_token_store()` | +| C++ | `.default_file_token_store()` | `.file_token_store(directory)` | + +Java, Python and Rust attach the store with `tokenStore(...)`, `token_store=` +and `.token_store(...)` respectively; the C and C++ calls above are builder +methods and attach it themselves. + +The default directory is `$HOME/.questdb/oidc-tokens/`, overridden by the +`questdb.client.oidc.token.store.dir` environment variable. Java reads that same +name as a JVM system property rather than an environment variable, and falls +back to `${user.home}/.questdb/oidc-tokens/`. On Unix the clients create token +files with mode `0600` and the store directory with `0700`; on other platforms +the directory's existing ACL governs access, so restrict it before enabling the +store. + +### Interaction-required errors + +A transport call never prompts. When the cached token cannot be refreshed +silently, the call fails instead, and the application has to sign in again on +the main or UI thread: + +| Client | How it surfaces | +| --- | --- | +| Python | `OidcInteractionRequired`, importable from `questdb.auth` | +| Rust | a `questdb::Error` carrying `ErrorCode::AuthError`; read `err.oidc_error()` and match on `OidcErrorKind::InteractionRequired` | +| C | error kind `QUESTDB_OIDC_ERROR_INTERACTION_REQUIRED` | +| C++ | `questdb::error_kind::interaction_required` | +| Java | `OidcAuthException`, with no distinct interaction-required type to match on | + +In C++ a failure raised through an attached sender arrives as +`questdb::ingress::line_sender_error` carrying `oidc_diagnostic()`, not as +`questdb::oidc::error`, so catch the common base `const questdb::error&` to +handle both. diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 085b88423a..719947d1c2 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -385,7 +385,7 @@ configuration document when `acl.oidc.configuration.url` is set. [`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint) is present when a device authorization endpoint has been configured, or when the provider advertises one. QuestDB publishes it without using it, for clients -which implement the [Device Authorization Flow](#device-authorization-flow) +which implement the [Device Authorization Flow](/docs/security/oidc-device-flow/) themselves. `acl.oidc.redirect.uri` is `null` unless @@ -397,7 +397,29 @@ enforces neither. The client generates the code verifier and the `state` value; the provider checks the verifier, and the client checks the `state` value it gets back. -### Which token to send +:::note + +Read `acl.oidc.enabled` before any of the other keys. The `acl.oidc.*` entries +are published whether or not OIDC is enabled, and with OIDC disabled the +endpoint URLs are built from the defaults rather than from a real provider. + +OIDC also requires [`acl.enabled`](/docs/configuration/iam/#aclenabled) to be +`true`, which the same response carries. When access control is disabled, +`acl.oidc.enabled` still reports the configured value while no OIDC +authentication takes place, so read both keys. + +::: + +The path of the endpoint is set by +[`http.context.settings`](/docs/configuration/http-server/#httpcontextsettings). +Setting it adds paths rather than moving the endpoint, so the default path keeps +working too. That default is `/settings` only while +[`http.context.web.console`](/docs/configuration/http-server/#httpcontextwebconsole) +is at its default, as the settings path follows the Web Console context path. +A client which cannot assume both are at their defaults should make the path +configurable. + +## Which token to send `acl.oidc.groups.encoded.in.token` tells the client which of the tokens returned by the provider belongs in the `Authorization: Bearer` header: @@ -427,379 +449,17 @@ for the full list of what is and is not validated. ::: -:::note - -Read `acl.oidc.enabled` before any of the other keys. The `acl.oidc.*` entries -are published whether or not OIDC is enabled, and with OIDC disabled the -endpoint URLs are built from the defaults rather than from a real provider. - -OIDC also requires [`acl.enabled`](/docs/configuration/iam/#aclenabled) to be -`true`, which the same response carries. When access control is disabled, -`acl.oidc.enabled` still reports the configured value while no OIDC -authentication takes place, so read both keys. - -::: - -The path of the endpoint is set by -[`http.context.settings`](/docs/configuration/http-server/#httpcontextsettings). -Setting it adds paths rather than moving the endpoint, so the default path keeps -working too. That default is `/settings` only while -[`http.context.web.console`](/docs/configuration/http-server/#httpcontextwebconsole) -is at its default, as the settings path follows the Web Console context path. -A client which cannot assume both are at their defaults should make the path -configurable. - ## Device Authorization Flow -The [OAuth 2.0 Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628) -signs in a person without redirecting a browser back to the client. It is a good -fit for command-line applications, containers, and remote notebook kernels: the -client displays a verification URL and short code, and the user can authorize -from a browser on any laptop or phone. - -QuestDB does not run the device flow. The client communicates directly with the -Identity Provider, then presents the resulting bearer token to QuestDB: - -```mermaid -sequenceDiagram - participant Client as QuestDB client - participant QDB as QuestDB - participant IdP as Identity Provider - participant User - Client->>QDB: GET /settings - QDB-->>Client: client ID, scope, endpoints, token mode - Client->>IdP: Request device and user codes - IdP-->>Client: device_code, user_code, verification URI - Client-->>User: Display URL and code - User->>IdP: Sign in and approve in a browser - loop Until approved or expired - Client->>IdP: Poll token endpoint - IdP-->>Client: authorization_pending / tokens - end - Client->>QDB: Authorization: Bearer selected token -``` - -The client must respect the polling interval, `slow_down` responses, and the -device code's expiry. The official clients handle those protocol details, -choose the access or ID token according to -[`acl.oidc.groups.encoded.in.token`](#which-token-to-send), cache it in memory, -and refresh it silently when the provider issues a refresh token. They never -send the device code or the user's Identity Provider password to QuestDB. - -### Configure the provider and QuestDB - -Before using the flow: - -1. Register the application as a public client with the Identity Provider and - enable the device authorization grant on it. Each application which - integrates via OIDC should have its own Client Id, so that it can be told - apart from QuestDB in the provider's audit log and given its own policies. - Pass that Client Id to the client explicitly, as described under - [Explicit configuration and endpoint pinning](#explicit-configuration-and-endpoint-pinning). -2. Make the device authorization endpoint available to clients. For the - zero-configuration examples below, `acl.oidc.configuration.url` must resolve - a provider document containing `device_authorization_endpoint`, or the - host-based configuration must set - [`acl.oidc.device.authorization.endpoint`](/docs/configuration/oidc/#acloidcdeviceauthorizationendpoint). -3. Keep `openid` in `acl.oidc.scope`. Add `offline_access` if your provider - requires that scope before issuing a refresh token to a device-flow client. - -For example, a host-based configuration can add: - -```ini title="server.conf" -acl.oidc.scope=openid offline_access -acl.oidc.device.authorization.endpoint=/as/device_authz.oauth2 -``` - -The exact endpoint path and refresh-token scope are provider-specific. With -configuration-document discovery, do not set the endpoint property: all -individual endpoint settings are ignored in that mode. - -If QuestDB does not publish the device authorization endpoint, pin the provider -with the client's `issuer` option so the client can fetch the provider's -`/.well-known/openid-configuration` document, or configure the client ID and -both endpoints explicitly. - -### Official client examples - -Each example discovers the OIDC client ID, scope, token-selection mode, and -provider endpoints from QuestDB's [settings endpoint](#settings-endpoint), then -signs in before opening the database connection. - -Discovering the client ID means reusing QuestDB's own registration, which keeps -the examples short but gives the application no separate identity at the -provider. That is fine for evaluation; in production register the application -and set its Client Id explicitly, as described below. - -The discovery URL must use `https`: reaching QuestDB over plaintext would let an -attacker replace the advertised Identity Provider endpoints and redirect the -device code and the refresh token, so the clients reject it rather than warn. -The Rust, C, C++ and Python clients accept loopback `http` for local -development; the Java client does not. To use a plaintext non-loopback host -anyway, opt in with `allowInsecureTransport(true)` (Java), -`.allow_insecure_transport(true)` (Rust), -`questdb_oidc_builder_allow_insecure_transport()` (C), -`allow_insecure_transport()` (C++) or `insecure=True` (Python). - - - - -```java -import io.questdb.client.QuestDB; -import io.questdb.client.cutlass.auth.OidcDeviceAuth; - -try (OidcDeviceAuth auth = OidcDeviceAuth.fromQuestDB( - "https://questdb.example.com:9000")) { - auth.signIn(); // the only call which may prompt or open a browser - - try (QuestDB db = QuestDB.connect( - "wss::addr=questdb.example.com:9000;", - auth::getToken)) { - // db.borrowSender() and db.borrowQuery() use the rotating token. - } -} -``` - - - - -```python -import questdb -from questdb.auth import OidcDeviceAuth - -with OidcDeviceAuth.from_questdb( - "https://questdb.example.com:9000") as auth: - auth.sign_in() # the only call which may prompt or open a browser - - with questdb.connect( - "wss::addr=questdb.example.com:9000;", - oidc_auth=auth) as db: - # db.sender(), db.dataframe(), and db.query() use the rotating token. - pass -``` - - - - -Enable the `oidc` crate feature: - -```toml title="Cargo.toml" -[dependencies] -questdb-rs = { version = "7", features = ["oidc"] } -``` - -```rust -use std::sync::Arc; -use questdb::{oidc::OidcDeviceAuth, QuestDb}; - -fn main() -> questdb::Result<()> { - let auth = Arc::new( - OidcDeviceAuth::from_questdb("https://questdb.example.com:9000") - .build()?, - ); - auth.sign_in()?; // the only call which may prompt or open a browser - - let db = QuestDb::connect_with_token_provider( - "wss::addr=questdb.example.com:9000;", - { - let auth = Arc::clone(&auth); - move || auth.token() - }, - )?; - // db.borrow_sender() and db.borrow_reader() use the rotating token. - Ok(()) -} -``` - - - - -```cpp -#include -#include -#include - -int main() { - auto auth = questdb::oidc::builder::from_questdb( - "https://questdb.example.com:9000") - .event_handler([](const questdb::oidc::event_view& event) { - if (event.kind() == questdb::oidc::event_kind::prompt) - std::cerr << "Open " << event.verification_uri() - << " and enter " << event.user_code() << '\n'; - }) - .build(); - - auth.sign_in(); // the only call which may prompt or open a browser - questdb::pool pool{"wss::addr=questdb.example.com:9000;", auth}; - // pool.borrow_sender() and pool.borrow_reader() use the rotating token. -} -``` - - - - -```c -#include -#include -#include -#include - -static void show_prompt(void *data, const questdb_oidc_event *event) { - (void)data; - if (event->kind == QUESTDB_OIDC_EVENT_PROMPT) - fprintf(stderr, "Open %.*s and enter %.*s\n", - (int)event->verification_uri_len, event->verification_uri, - (int)event->user_code_len, event->user_code); -} - -int main(void) { - questdb_error *error = NULL; - questdb_oidc_auth *auth = NULL; - const char *url = "https://questdb.example.com:9000"; - questdb_oidc_builder *builder = - questdb_oidc_builder_from_questdb(url, strlen(url), &error); - if (!builder || !questdb_oidc_builder_event_handler( - builder, show_prompt, NULL, NULL, &error)) - goto fail; - - auth = questdb_oidc_builder_build(builder, &error); - if (!auth || !questdb_oidc_auth_sign_in(auth, &error)) - goto fail; - - questdb_db_connect_options options; - questdb_db_connect_options_init(&options, sizeof options); - options.oidc_auth = auth; - - const char *conf = "wss::addr=questdb.example.com:9000;"; - questdb_db *db = questdb_db_connect_ex( - conf, strlen(conf), &options, &error); - if (!db) - goto fail; - - /* Sender and reader borrows use the rotating token. */ - questdb_db_close(db); - questdb_oidc_auth_free(auth); - questdb_oidc_builder_free(builder); - return 0; - -fail: - if (error) { - size_t len = 0; - const char *message = questdb_error_msg(error, &len); - fprintf(stderr, "OIDC sign-in failed: %.*s\n", (int)len, message); - } - questdb_error_free(error); - questdb_oidc_auth_free(auth); - questdb_oidc_builder_free(builder); - return 1; -} -``` - - - - -The authentication object owns the token state. Pass it as a rotating provider, -as shown above, instead of copying the current token into the connection string; -otherwise a reconnect after token expiry will keep sending the stale value. - -Device flow always requires a person to approve the sign-in. For unattended -services, scheduled jobs, or CI, use a service-account token or a provider flow -intended for machine identities instead. - -#### The sign-in prompt - -Signing in renders the verification URL and the user code, then blocks until the -user approves or the device code expires. Where that text goes, and whether the -call runs at all without a terminal, differs by client: +For command-line applications, containers, and remote notebook kernels, where +no browser redirect can reach the client, QuestDB Enterprise supports the +OAuth 2.0 Device Authorization Grant. QuestDB does not run the flow: the client +talks to the Identity Provider directly and presents the resulting bearer +token. -| Client | Default prompt | Without a terminal | -| --- | --- | --- | -| Java | prints to `System.out`, then tries to open a browser | runs anyway; the client performs no terminal check | -| Python | prints to stderr, or to the frontend inside an IPython kernel | fails, unless the kernel accepts stdin | -| Rust, C, C++ | prints to stderr | fails with an interaction-required error | - -Redirecting output therefore hides the code in Java, while a container with no -TTY fails outright in the other four. Override the default to render the -challenge elsewhere, or to declare the process interactive anyway: - -| Client | Custom prompt | Force interactive | -| --- | --- | --- | -| Java | `prompt(...)`, or `DeviceCodePrompt.SYSTEM_OUT` to skip the browser launch | not applicable | -| Python | `renderer=` | `interactive=True` | -| Rust | `.renderer(...)` | `.interactive(true)` | -| C | `questdb_oidc_builder_event_handler()` | `questdb_oidc_builder_interactive()` | -| C++ | `.event_handler(...)` | `.interactive(true)` | - -The prompt's text fields are display-safe. Where a client opens the URL or makes -it clickable, use the separately vetted browser target instead: `browser_target` -on the C event struct, `event_view::browser_target()` in C++. - -#### Explicit configuration and endpoint pinning - -Discovery supplies the client ID, scope, audience, token-selection mode, and -endpoints. Set any of them explicitly when the application has its own -registration with the provider, or when QuestDB does not publish what the client -needs: - -| Setting | Set it when | -| --- | --- | -| `client_id` | the application has its own registration, which is the recommended setup | -| `scope` | the provider requires `offline_access` before it issues a refresh token | -| `audience` | the provider requires one, or QuestDB validates `aud` against a non-default [`acl.oidc.audience`](/docs/configuration/oidc/#acloidcaudience). QuestDB does not publish that key, so discovery cannot supply it | -| `issuer` | QuestDB publishes no device authorization endpoint, so the client reads the provider's `/.well-known/openid-configuration` instead | -| `token_endpoint`, `device_authorization_endpoint` | pinning both endpoints directly rather than through an issuer | -| `groups_in_token` | overriding which of the two tokens the client selects | - -Java exposes `issuer`, `prompt`, `tokenStore`, `tlsConfig` and -`allowInsecureTransport` on `OidcDeviceAuth.DiscoveryOptions`, and the full set -on `OidcDeviceAuth.builder()`, in camel case. Python passes them as keyword -arguments to `from_questdb`. Rust and C++ are builder methods in snake case, and -C prefixes the same names with `questdb_oidc_builder_`. - -#### Token persistence - -Tokens stay in memory unless a file token store is enabled. The store writes the -access, ID and long-lived refresh tokens as unencrypted JSON, so turn it on only -where that at-rest exposure is acceptable: - -| Client | Default location | A directory you choose | -| --- | --- | --- | -| Java | `FileTokenStore.atDefaultLocation()` | `FileTokenStore.at(path)` | -| Python | `FileTokenStore.at_default_location()` | `FileTokenStore.at(path)` | -| Rust | `FileTokenStore::at_default_location()?` | `FileTokenStore::at(path)` | -| C | `questdb_oidc_builder_default_file_token_store()` | `questdb_oidc_builder_file_token_store()` | -| C++ | `.default_file_token_store()` | `.file_token_store(directory)` | - -Java, Python and Rust attach the store with `tokenStore(...)`, `token_store=` -and `.token_store(...)` respectively; the C and C++ calls above are builder -methods and attach it themselves. - -The default directory is `$HOME/.questdb/oidc-tokens/`, overridden by the -`questdb.client.oidc.token.store.dir` environment variable. Java reads that same -name as a JVM system property rather than an environment variable, and falls -back to `${user.home}/.questdb/oidc-tokens/`. On Unix the clients create token -files with mode `0600` and the store directory with `0700`; on other platforms -the directory's existing ACL governs access, so restrict it before enabling the -store. - -#### Interaction-required errors - -A transport call never prompts. When the cached token cannot be refreshed -silently, the call fails instead, and the application has to sign in again on -the main or UI thread: - -| Client | How it surfaces | -| --- | --- | -| Python | `OidcInteractionRequired`, importable from `questdb.auth` | -| Rust | a `questdb::Error` carrying `ErrorCode::AuthError`; read `err.oidc_error()` and match on `OidcErrorKind::InteractionRequired` | -| C | error kind `QUESTDB_OIDC_ERROR_INTERACTION_REQUIRED` | -| C++ | `questdb::error_kind::interaction_required` | -| Java | `OidcAuthException`, with no distinct interaction-required type to match on | - -In C++ a failure raised through an attached sender arrives as -`questdb::ingress::line_sender_error` carrying `oidc_diagnostic()`, not as -`questdb::oidc::error`, so catch the common base `const questdb::error&` to -handle both. +See [OIDC Device Authorization Flow](/docs/security/oidc-device-flow/) for the +protocol walkthrough, what to configure first, and worked examples for the +Java, Python, Rust, C and C++ clients. ## Interactive clients @@ -850,7 +510,7 @@ The OAuthenticator documentation also contains using different identity providers. If Jupyter notebooks are used without JupyterHub, use the official Python -client's [Device Authorization Flow](#device-authorization-flow). It works when +client's [Device Authorization Flow](/docs/security/oidc-device-flow/). It works when the browser and notebook kernel are on different machines and does not put the user's password in the notebook. @@ -1015,7 +675,7 @@ with pg.connect(conn_str, autocommit=True) as connection: ### CLI, standalone applications For a CLI or standalone application built with an official Java, Rust, C, or -C++ client, use the [Device Authorization Flow](#device-authorization-flow). +C++ client, use the [Device Authorization Flow](/docs/security/oidc-device-flow/). Tools such as `psql` and Microsoft Access cannot run the flow themselves; for those tools, acquire a token separately and use [PGWire token authentication](#oidc-for-the-pgwire-endpoint), or enable the diff --git a/documentation/sidebars.js b/documentation/sidebars.js index e0a4b89577..57525fb832 100644 --- a/documentation/sidebars.js +++ b/documentation/sidebars.js @@ -723,6 +723,11 @@ module.exports = { type: "doc", label: "OpenID Connect (OIDC)", }, + { + id: "security/oidc-device-flow", + type: "doc", + label: "OIDC device flow", + }, { type: "doc", id: "security/tls", From f3cb07c1caf3e1a22110864631a92780fc6c0dc7 Mon Sep 17 00:00:00 2001 From: glasstiger Date: Thu, 3 Sep 2026 01:17:49 +0100 Subject: [PATCH 38/38] Correct the remaining OIDC inaccuracies and inconsistencies Factual corrections, each checked against the server or the Web Console: - acl.oidc.enabled=false does not take the settings out of play. The configuration URL is still parsed and its scheme still validated, so a malformed value fails startup with OIDC switched off. - With acl.oidc.configuration.url set and OIDC disabled, the endpoint keys are absent from the settings response rather than built from the defaults. Only the host-configured case defaults them. - The settings example and its key table omitted acl.basic.auth.realm.enabled, which the server emits between acl.enabled and acl.oidc.enabled. Both now match SettingsEndpointTest key for key and in order. - The Web Console falls back to the location it was loaded from without the query string or fragment, not window.location.href. - Step 7 of the walkthrough was generalised to either token, but the sentence about the user info cache still said "the access token". - The CLI section listed Java, Rust, C and C++ and omitted Python, which two other lists on the same page and the configuration reference include. Consistency: - The Rust crate-features table gained the oidc row it documents elsewhere. - http.context.settings said its default is /settings, four paragraphs above the prose explaining that this holds only while http.context.web.console is at its default. Qualified inline, where a config scraper will read it. - The two sidebar entries both rendered "OpenID Connect (OIDC)". They are now the guide, the device flow, and the settings reference. - nodejs.md was the one client page with no OIDC route, and its Enterprise auth link led to a page that does not mention OIDC. - The Implicit flow is marked deprecated: it cannot use PKCE, which the settings endpoint advertises as required by default. - Three same-page jumps were written as third-person cross-page references to "the OIDC operations document", which is the page the reader is on. - The two non-interactive examples used the legacy ILP Sender over http while the Python client page teaches questdb.connect over wss. - server.conf blocks were fenced as shell in three places and ini in another. - The QuestDB host placeholder was questdb.host in the older examples and questdb.example.com in the new ones; the query example also used port 9999. Nits left alone deliberately: "Device Authorization Flow" keeps its title case, matching the sibling "Authentication and Authorization Flow" and RFC 8628's own term, and renaming it would break the anchor the extraction preserved. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY --- documentation/configuration/http-server.md | 3 +- documentation/configuration/oidc.md | 12 ++- documentation/connect/clients/nodejs.md | 7 ++ documentation/connect/clients/rust.md | 1 + documentation/security/oidc-device-flow.mdx | 2 +- documentation/security/oidc.mdx | 83 ++++++++++++--------- documentation/sidebars.js | 2 +- 7 files changed, 66 insertions(+), 44 deletions(-) diff --git a/documentation/configuration/http-server.md b/documentation/configuration/http-server.md index fec0b1848d..76e968d7a6 100644 --- a/documentation/configuration/http-server.md +++ b/documentation/configuration/http-server.md @@ -515,7 +515,8 @@ Context path for the file import service. ### http.context.settings -- **Default**: `/settings` +- **Default**: `/settings`, relative to + [`http.context.web.console`](#httpcontextwebconsole) - **Reloadable**: no Context path for the service that serves server-side settings to clients, such diff --git a/documentation/configuration/oidc.md b/documentation/configuration/oidc.md index 08cc01049d..20820b5c1a 100644 --- a/documentation/configuration/oidc.md +++ b/documentation/configuration/oidc.md @@ -1,5 +1,6 @@ --- title: OpenID Connect (OIDC) +sidebar_label: OIDC settings description: "QuestDB Enterprise acl.oidc.* settings reference: minimum configuration and startup rules, endpoints, TLS, user and group claims, caching and buffers." --- @@ -23,13 +24,15 @@ which is the default. With access control disabled no OIDC authentication takes place, but the OIDC settings are still validated at startup: with `acl.oidc.enabled=true` the server enforces every rule below and downloads the provider's configuration document, so an inconsistent OIDC configuration still -prevents it from starting. Set `acl.oidc.enabled=false` to take the settings out -of play entirely. +prevents it from starting. Set `acl.oidc.enabled=false` to stop the rules below being +enforced. `acl.oidc.configuration.url` is still parsed even then, and its +scheme must still match [`acl.oidc.tls.enabled`](#acloidctlsenabled), so a +malformed value there fails startup with OIDC switched off. A working setup against a Ping Identity provider needs four settings. Every other setting has a usable default: -```shell +```ini title="server.conf" acl.oidc.enabled=true acl.oidc.host=oidc.provider acl.oidc.client.id=questdb @@ -141,7 +144,8 @@ set, because the port is taken from the discovered endpoint URLs. The redirect URI tells the OIDC server where to redirect the user after successful authentication. If not set, the Web Console defaults it to the -location where it was loaded from (`window.location.href`). +location it was loaded from, without the query string or fragment +(`window.location.origin + window.location.pathname`). ### acl.oidc.scope diff --git a/documentation/connect/clients/nodejs.md b/documentation/connect/clients/nodejs.md index de60fe5610..0df1db8f1a 100644 --- a/documentation/connect/clients/nodejs.md +++ b/documentation/connect/clients/nodejs.md @@ -91,6 +91,13 @@ When using QuestDB Enterprise, authentication can also be done via REST token. Please check the [RBAC docs](/docs/security/rbac/#authentication) for more info. +This client does not run an OIDC flow. QuestDB Enterprise also accepts an OIDC +bearer token, which you acquire out of band from your Identity Provider: +QuestDB publishes the provider's endpoints on its +[settings endpoint](/docs/security/oidc/#settings-endpoint), and the same +response tells you [which token to send](/docs/security/oidc/#which-token-to-send). +See [OpenID Connect](/docs/security/oidc/). + ## Basic insert Example: inserting executed trades for cryptocurrencies. diff --git a/documentation/connect/clients/rust.md b/documentation/connect/clients/rust.md index fa467b4267..f97eaa9ddc 100644 --- a/documentation/connect/clients/rust.md +++ b/documentation/connect/clients/rust.md @@ -948,6 +948,7 @@ integrations only when your application uses them: | `ndarray` | No | `Buffer::column_arr` from `ndarray` views. | | `rust_decimal` / `bigdecimal` | No | Row-buffer decimal values from those crates. Decimal strings need neither feature. | | `chrono-timestamp` | No | Timestamp values built from `chrono::DateTime`. | +| `oidc` | No | Interactive OIDC sign-in with the [device flow](/docs/security/oidc-device-flow/). Pulls in `sync-sender-http`, and needs a TLS root source for `https` discovery. | | `tls-native-certs` | No | TLS validation through the operating-system certificate store. | | `insecure-skip-verify` | No | `tls_verify=unsafe_off` for controlled testing only. | | `almost-all-features` | No | Client development and testing with most compatible features. It excludes Arrow and Polars. | diff --git a/documentation/security/oidc-device-flow.mdx b/documentation/security/oidc-device-flow.mdx index 5b1b4d25d1..091acede13 100644 --- a/documentation/security/oidc-device-flow.mdx +++ b/documentation/security/oidc-device-flow.mdx @@ -36,7 +36,7 @@ sequenceDiagram QDB-->>Client: client ID, scope, endpoints, token mode Client->>IdP: Request device and user codes IdP-->>Client: device_code, user_code, verification URI - Client-->>User: Display URL and code + Client->>User: Display URL and code User->>IdP: Sign in and approve in a browser loop Until approved or expired Client->>IdP: Poll token endpoint diff --git a/documentation/security/oidc.mdx b/documentation/security/oidc.mdx index 719947d1c2..1d76f1f6e5 100644 --- a/documentation/security/oidc.mdx +++ b/documentation/security/oidc.mdx @@ -125,7 +125,7 @@ been authenticated already: Identity Provider for authentication. ```bash title="Authorization code request example" -https://oidc.provider:443/as/authorization.oauth2?client_id=questdb&response_type=code&scope=openid&redirect_uri=https%3A%2F%2Fquestdb.host%3A9000&code_challenge=IwZ-WuypAY3fMtvismbj1MQUe5CzMgrBa87nYcgFoLQ&code_challenge_method=S256 +https://oidc.provider:443/as/authorization.oauth2?client_id=questdb&response_type=code&scope=openid&redirect_uri=https%3A%2F%2Fquestdb.example.com%3A9000&code_challenge=IwZ-WuypAY3fMtvismbj1MQUe5CzMgrBa87nYcgFoLQ&code_challenge_method=S256 ``` :::note @@ -191,7 +191,7 @@ The Authorization Server redirects the user back to the [Web Console](/docs/getting-started/web-console/overview/) with the _authorization code_: ```bash title="Authorization code response example" -https://questdb.host:9000/?code=1L344XEY5XRka1j4ySNa8bVQSLf71as9uGLEuv_A +https://questdb.example.com:9000/?code=1L344XEY5XRka1j4ySNa8bVQSLf71as9uGLEuv_A ``` ### 5. Credential request @@ -213,7 +213,7 @@ which requested the authorization code, and it was not stolen: ```bash title="Token request example" POST https://oidc.provider:443/as/token.oauth2 HTTP/1.1 Content-Type: application/x-www-form-urlencoded -grant_type=authorization_code&code=1L344XEY5XRka1j4ySNa8bVQSLf71as9uGLEuv_A&client_id=questdb&&redirect_uri=https%3A%2F%2Fquestdb.host%3A9000&code_verifier=uGZh4sQffXLgRna7D-jtEAkuXzp7Lm_okZXBljzP38coAD44kEheIaz7Pdh98KxYtYLZHNiQPCczQYeF +grant_type=authorization_code&code=1L344XEY5XRka1j4ySNa8bVQSLf71as9uGLEuv_A&client_id=questdb&&redirect_uri=https%3A%2F%2Fquestdb.example.com%3A9000&code_verifier=uGZh4sQffXLgRna7D-jtEAkuXzp7Lm_okZXBljzP38coAD44kEheIaz7Pdh98KxYtYLZHNiQPCczQYeF ``` ### 6. Credentials received @@ -248,20 +248,24 @@ A token is in the header of every request sent to QuestDB. Which of the two goes there depends on where QuestDB reads the group memberships from, see [Which token to send](#which-token-to-send). -> **Worried about exposing the token?** The access token is rather opaque and -> does not contain user details. The ID token does: it is a JWT whose payload is -> base64 encoded, not encrypted, so treat it as you would the user's directory -> record. +:::note + +Worried about exposing the token? An access token is usually opaque and carries +no user details, though some providers issue a JWT here too. An ID token always +does: it is a JWT whose payload is base64 encoded, not encrypted, so treat it as +you would the user's directory record. + +::: To carry out permission checks, the database has to know more about the user. For this, QuestDB has a User Info Cache. -If it finds a valid entry with the access token in the cache, steps 8 and 9 are +If it finds a valid entry for the token in the cache, steps 8 and 9 are skipped: ```bash title="Query request example" -https://questdb.host:9999/exec?query=select%20current_user() +https://questdb.example.com:9000/exec?query=select%20current_user() Authorization: Bearer gslpJtzmmi6RwaPSx0dYGD4tEkom ``` @@ -333,7 +337,7 @@ order to start an authentication flow. Any client can read them from the settings endpoint, which requires no authentication: ```bash title="Settings request example" -curl https://questdb.host:9000/settings +curl https://questdb.example.com:9000/settings ``` The OIDC-related entries of the response look like this, alongside other server @@ -343,12 +347,13 @@ settings: { "config": { "acl.enabled": true, + "acl.basic.auth.realm.enabled": false, "acl.oidc.enabled": true, "acl.oidc.pkce.required": true, "acl.oidc.state.required": false, "acl.oidc.groups.encoded.in.token": false, "acl.oidc.client.id": "questdb", - "acl.oidc.redirect.uri": "https://questdb.host:9000", + "acl.oidc.redirect.uri": "https://questdb.example.com:9000", "acl.oidc.scope": "openid", "acl.oidc.authorization.endpoint": "https://oidc.provider:443/as/authorization.oauth2", "acl.oidc.token.endpoint": "https://oidc.provider:443/as/token.oauth2", @@ -368,6 +373,7 @@ hard coding the provider's details. | Key | Type | Present | | --- | --- | --- | | `acl.enabled` | boolean | always | +| `acl.basic.auth.realm.enabled` | boolean | always | | `acl.oidc.enabled` | boolean | always | | `acl.oidc.pkce.required` | boolean | always | | `acl.oidc.state.required` | boolean | always | @@ -400,8 +406,10 @@ gets back. :::note Read `acl.oidc.enabled` before any of the other keys. The `acl.oidc.*` entries -are published whether or not OIDC is enabled, and with OIDC disabled the -endpoint URLs are built from the defaults rather than from a real provider. +are published whether or not OIDC is enabled. With OIDC disabled and +`acl.oidc.host` in use, the endpoint URLs are built from the defaults rather +than from a real provider; with `acl.oidc.configuration.url` set they are not +resolved at all, and the endpoint keys are absent from the response. OIDC also requires [`acl.enabled`](/docs/configuration/iam/#aclenabled) to be `true`, which the same response carries. When access control is disabled, @@ -484,7 +492,9 @@ possible flows to request an access token.: flow **(Recommended, more secure)** 2. [Implicit](https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth) - flow + flow **(deprecated)**. It cannot use PKCE, so it does not satisfy + [`acl.oidc.pkce.required`](/docs/configuration/oidc/#acloidcpkcerequired), + which is `true` by default. The client can read the authorization and token endpoints, the client id and the scopes from the [settings endpoint](#settings-endpoint) instead of hard @@ -674,8 +684,9 @@ with pg.connect(conn_str, autocommit=True) as connection: ### CLI, standalone applications -For a CLI or standalone application built with an official Java, Rust, C, or -C++ client, use the [Device Authorization Flow](/docs/security/oidc-device-flow/). +For a CLI or standalone application built with an official Java, Python, Rust, +C, or C++ client, use the +[Device Authorization Flow](/docs/security/oidc-device-flow/). Tools such as `psql` and Microsoft Access cannot run the flow themselves; for those tools, acquire a token separately and use [PGWire token authentication](#oidc-for-the-pgwire-endpoint), or enable the @@ -726,8 +737,8 @@ applies to the connection below, which carries the resulting token. import os import requests import pandas as pd +import questdb from dotenv import load_dotenv -from questdb.ingress import Sender load_dotenv() user = os.environ.get("username") @@ -758,11 +769,11 @@ if token_key not in tokens: raise SystemExit(f"the provider did not issue an {token_key} for the password grant") token = tokens[token_key] -conf = f"https::addr=questdb.example.com:9000;token={token};" -with Sender.from_conf(conf) as sender: - df = pd.read_csv("data.csv") - df["ts"] = pd.to_datetime(df["ts"]) - sender.dataframe(df, table_name="foo", at="ts") +conf = f"wss::addr=questdb.example.com:9000;token={token};" +with questdb.connect(conf) as db: + df = pd.read_csv("trades.csv") + df["timestamp"] = pd.to_datetime(df["timestamp"]) + db.dataframe(df, table_name="trades", symbols=["symbol"], at="timestamp") ``` :::note @@ -782,18 +793,18 @@ server side: ```python import os import pandas as pd +import questdb from dotenv import load_dotenv -from questdb.ingress import Sender load_dotenv() user = os.environ.get("username") pwd = os.environ.get("password") -conf = f"https::addr=questdb.example.com:9000;username={user};password={pwd};" -with Sender.from_conf(conf) as sender: - df = pd.read_csv("data.csv") - df["ts"] = pd.to_datetime(df["ts"]) - sender.dataframe(df, table_name="foo", at="ts") +conf = f"wss::addr=questdb.example.com:9000;username={user};password={pwd};" +with questdb.connect(conf) as db: + df = pd.read_csv("trades.csv") + df["timestamp"] = pd.to_datetime(df["timestamp"]) + db.dataframe(df, table_name="trades", symbols=["symbol"], at="timestamp") ``` ## OIDC for the PGWire endpoint @@ -1255,9 +1266,8 @@ The value is `memberOf`. #### Enable Resource Owner Password Credentials (ROPC) flow -As described in the -[OIDC operations document](/docs/security/oidc/#enable-ropc) -tools - such as `psql` - can be integrated with the OIDC provider using the ROPC flow. +As described under [Enable ROPC](#enable-ropc), tools such as `psql` can be +integrated with the OIDC provider using the ROPC flow. When setting this flow up, enable the Resource Owner Password Credentials flow in the client settings. @@ -1274,7 +1284,7 @@ Then select the `username` attribute of the PCV as `USER_KEY`. The below should be set in QuestDB's `server.conf`: -```shell +```ini title="server.conf" # enable OIDC acl.oidc.enabled=true @@ -1303,8 +1313,8 @@ set. #### Confirm QuestDB mappings and login -QuestDB requires a mapping, as laid out in the -[OIDC operations document](/docs/security/oidc/#mapping-user-permissions). +QuestDB requires a mapping, as laid out under +[Mapping user permissions](#mapping-user-permissions). If a given user has the HTTP permission, they will be able to now login via the [Web Console](/docs/getting-started/web-console/overview/). @@ -1438,8 +1448,7 @@ validate the ID token, and take the group information from there. QuestDB authorization relies on receiving the group memberships of the user. Entra ID groups should be mapped to QuestDB groups, and permissions can be granted to the QuestDB groups. Detailed information about group mappings can -be found in the [OIDC integration](/docs/security/oidc/#user-permissions) -documentation. +be found under [User permissions](#user-permissions).