Skip to content

docs: rework the OIDC documentation and split the OIDC and RBAC guides into sections - #522

Open
glasstiger wants to merge 55 commits into
mainfrom
ia_oidc_config_options
Open

docs: rework the OIDC documentation and split the OIDC and RBAC guides into sections#522
glasstiger wants to merge 55 commits into
mainfrom
ia_oidc_config_options

Conversation

@glasstiger

@glasstiger glasstiger commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

What

The OIDC documentation, in five parts: the Device Authorization Flow, which
was not documented at all and now has its own page; the settings endpoint a
client discovers the provider from; the configuration reference, which was
missing five options and described four others wrongly; a pass over the pages
elsewhere in the docs that contradicted any of it; and a restructure that
breaks the two 700-1600 line security pages into browsable sections.

Important

This is blocked on the client releases. See
Release coordination at the bottom. The device-flow
APIs documented here are correct, but none of them has shipped, and the Rust
snippet does not build against the published crate.

Device Authorization Flow

A new page, OIDC Device Authorization Flow,
for command-line applications, containers, and remote notebook kernels, where no
browser redirect can come back to the client. It has its own sidebar entry; the
guide keeps a short section at the old #device-authorization-flow anchor so
existing links still resolve.

  • QuestDB does not run the flow. The client talks to the Identity Provider
    directly and then presents the resulting bearer token, which the sequence
    diagram walks through end to end. The client is responsible for the polling
    interval, slow_down, and the device code's expiry.
  • What has to be configured first: the device grant enabled on a public
    client registered for the application, a device authorization endpoint QuestDB
    can publish (from the configuration document, or from
    acl.oidc.device.authorization.endpoint), and offline_access in
    acl.oidc.scope where the provider requires it before issuing a refresh
    token.
  • Worked examples for Java, Python, Rust, C++, and C. Each discovers the
    client ID, scope, endpoints, and token mode from /settings, signs in, then
    writes a row through the rotating token. They live in
    documentation/partials/ and are imported by both this page and the client
    page for that language, so the two copies cannot drift.
  • The discovery URL must be https. The clients reject plaintext rather
    than warn. Rust, C, C++ and Python make an exception for loopback; Java does
    not. The per-language opt-out is documented.

Four sections cover what the examples do not show, because the client pages were
already pointing at them:

  • The sign-in prompt. The five clients differ in ways that decide whether a
    headless job works at all: 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. Custom-prompt and
    force-interactive setters are named for each.
  • Explicit configuration and endpoint pinning. What each setting is for and
    where the setters live, including that Java splits them between
    DiscoveryOptions and builder().
  • Token persistence. The per-language constructor and attachment, the
    default directory and its override, and the 0600/0700 permissions, so the
    at-rest tradeoff the page asks the reader to accept can be judged.
  • Interaction-required errors. Java has no distinct type, and Rust flattens
    the kind to ErrorCode::AuthError, reachable only through err.oidc_error().

Each client page gained an OIDC device flow (Enterprise) section
(Java,
Python,
Rust,
C & C++). On the Python
and C/C++ pages this replaces the "Unsupported auth paths" table: OIDC token
acquisition and mid-session rotation are no longer unsupported there, so the
table is now a short list of what genuinely remains - mutual TLS, and a fixed
token= that does not rotate.

The guide's Jupyter and CLI sections point at the device flow first. ROPC is
demoted to the fallback for tooling that cannot run the flow, such as psql.

Settings endpoint

The settings endpoint
is what a client reads, without authentication, to discover the authorization,
token, and device authorization endpoints, the client ID and scope, and whether
PKCE and state are required. The new section documents the response key by
key, which keys are omitted when unset, and two traps: acl.oidc.enabled has to
be read before the rest, because the acl.oidc.* keys are published from the
defaults even when OIDC is off, and the path follows the Web Console context
path rather than being fixed at /settings.

Which token to send
is a top-level section rather than a subsection of the settings endpoint, since
nine pages link to it as the canonical answer for the Authorization header. It
documents what acl.oidc.groups.encoded.in.token means for a client, and now
carries the security consequence that previously lived only in the
configuration reference: with the setting enabled QuestDB validates the token
itself and never asks the provider about it, so a revoked token keeps working
until its exp passes.

http.context.settings
was rewritten to match: it serves clients rather than only the Web Console, it
takes a comma-separated list, and setting it adds paths instead of moving the
service. The Go,
.NET,
Node.js, and
QWP ingress
pages link to it, and the first two also link the device flow, since neither
client runs one and the settings endpoint alone does not tell them how to get a
token.

Configuration reference

Compared the acl.oidc.* keys defined in EntPropertyKey.java against
the OIDC configuration page. The
server defines 29 options, the page documented 24 names, one of which the server
does not recognise. All 29 are now documented.

Added

Option Default Notes
acl.oidc.state.required false Requires the state parameter in the Authorization Code Flow. Published to clients through the settings endpoint, same as the PKCE flag.
acl.oidc.device.authorization.endpoint none No default, and QuestDB never calls it. It is resolved and published for clients which run the Device Authorization Flow themselves.
acl.oidc.public.keys.expiry 120000 JWKS cache expiry in milliseconds. Also accepts a duration, unlike the other two millisecond settings.
acl.oidc.response.buffer.size 1M Buffer for HTTP responses from the OIDC Provider.
acl.oidc.string.pool.capacity 128 String pool used to parse the provider's JSON responses.

Corrected

  • acl.oidc.pkce.enabled does not exist. The property is
    acl.oidc.pkce.required. There is no alias, so anyone who copied the
    documented name got silently ignored config, with PKCE staying on.
  • acl.oidc.groups.claim has no default. The page claimed groups, but the
    default is null and the option is mandatory when OIDC is enabled - the
    server refuses to start without it.
  • acl.oidc.host and acl.oidc.configuration.url are mutually exclusive.
    Setting both fails server startup, which the page did not mention.
  • The public keys are downloaded whether or not they are used. The store is
    built whenever OIDC is enabled and loads the keys from its constructor. Only
    the use of the keys is conditional.
  • A missing groups claim fails authentication. The guide said the user is
    authenticated but ends up with no permissions. verifyTokenSlow returns false
    when either the groups or the sub claim is missing or empty, in both flows, so
    the login is rejected: 401 over HTTP, an authentication error on PGWire,
    with the reason only in the server log.
  • Two components read the token when
    acl.oidc.groups.encoded.in.token=true. The signature validator requires
    sub, aud and exp and checks the signature, audience and expiry against
    them; it never looks at the group memberships. QuestDB then reads the
    principal and the groups out of the same payload through
    acl.oidc.sub.claim and acl.oidc.groups.claim, exactly as in the user info
    flow. So acl.oidc.groups.claim may name any claim the token carries,
    roles for example, and acl.oidc.sub.claim may name any claim too as long
    as the token still carries sub for the validator. The claim's value is
    normally an array; a bare string is accepted where a provider sends a lone
    group.

Scoped to what the server actually does

  • acl.oidc.audience is only checked when the groups are encoded in the token.
    It is also the one discovery key the clients look for that the server never
    publishes, so a provider that requires an audience needs it set on the client.
  • The mandatory openid in acl.oidc.scope is enforced by the provider, not at
    startup.
  • acl.oidc.ropc.flow.enabled makes QuestDB itself exchange HTTP basic and
    PGWire credentials for a token at the provider.
  • When QuestDB validates a token itself it checks the signature, the audience,
    the expiry and the presence of the claims - but not nbf or iss. It never
    asks the provider, so revocation does not take effect until exp passes.
  • acl.basic.auth.realm.enabled cannot be combined with acl.oidc.enabled;
    setting both fails startup. Noted on the
    IAM page.
  • acl.oidc.enabled=false does not take the settings out of play entirely:
    acl.oidc.configuration.url is still parsed and its scheme still validated.

Restructured

  • A new Minimum configuration section: the four settings a working setup
    needs, and the full list of consistency rules the server enforces at startup.
    It also records that the OIDC settings are validated even with acl.enabled
    off, so a bad OIDC config still stops the server from starting.
  • A new Caching and buffers section. acl.oidc.cache.ttl moved there out of
    "User and group claims", next to the other caching and buffer settings, with
    what setting it to 0 does in each flow.

Cross-page reconciliation

Pages elsewhere in the docs contradicted the above, or predated OIDC entirely:

  • connect-string
    is where the client pages send readers for the credential grammar and listed
    only username, password and token. It now records that device-flow
    credentials are not connect-string keys, and that a static token= does not
    rotate.
  • RBAC counted three
    authentication methods and the
    REST API
    counted two; both omitted OIDC. The REST API page also now distinguishes an
    OIDC bearer token from the REST API token it documents below, since the two
    share the Authorization: Bearer header.
  • Failover told
    operators to send "an OIDC access token", which under the
    groups.encoded.in.token=true that the Entra ID walkthrough sets is rejected
    with a 401.
  • The Client Id rule had three answers across the guide. It is stated once, in
    the device flow's setup steps.

Guide corrections

  • Noted in the
    authorization code request step
    that some Identity Providers require the state parameter.
  • Added the missing QuestDB server.conf block to the
    PingFederate walkthrough,
    which relied on defaults for two settings that have none.
  • Renamed both "QuestDB configuration" headings to name their provider, pinning
    the Entra ID anchor so existing links keep working.
  • 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. Both
    non-interactive examples now use https, and were ported off the legacy ILP
    Sender to questdb.connect over wss, matching the Python client page.
  • The Implicit flow is marked deprecated: it cannot use PKCE, which the settings
    endpoint advertises as required by default.

Page restructure

The OIDC and RBAC guides had each grown into a single page too long to navigate
or link into precisely - oidc.mdx at 1600 lines, rbac.md at 717. Both are
now directories, one page per topic, with the overview at the section root and
a sidebar category listing the rest.

Was Now
security/oidc.mdx security/oidc/ - overview, how sign-in works, client discovery and tokens, client integration patterns, mapping groups and permissions, PingFederate, Entra ID
security/rbac.md security/rbac/ - overview, users and groups, authentication and endpoints, granting permissions, permissions reference, common scenarios

Old anchors keep working. Both section roots end with a "where each section
moved" table that holds the old anchor ids and points at the page each section
went to, so external links to #which-token-to-send,
#questdb-configuration-for-pingfederate, #endpoint-permissions and the rest
still land somewhere useful. Every inbound link inside the docs was repointed at
the new canonical anchor rather than left on the redirect.

Two permission statements were corrected while passing through the RBAC pages:
REBASE WAL
and SWITCH COLD STORAGE ROLE
need the SYSTEM ADMIN permission, which is grantable. Both pages described it
as unspecified "database administrator (system admin) privileges", and the cold
storage page went further and called the statements "not grantable through RBAC
permissions". Separately, all_permissions() lists every permission the
database supports, not the ones currently applied.

Verification

Every default was read off EntPropServerConfiguration.java:284-320, not
inferred, and the value formats off the getters each one uses. The
startup-failure claims were each checked against the validation in
EntPropServerConfiguration and OidcEndpoint. The token-validation table
describes the next Enterprise release, which validates exp; the earlier
"an expired token is still accepted" wording matched the shipped validator and
no longer holds. nbf and iss remain unchecked. The settings endpoint example
matches SettingsEndpointTest, key for key and in order, including
acl.basic.auth.realm.enabled.

The client API surfaces were checked against the client sources, and the
examples compile: Java with javac against the built client, Rust with
cargo check, and both C variants and the C++ example with -fsyntax-only -Wall -Wextra. Every type, method, kwarg and C symbol named in these pages
exists and is spelled correctly.

yarn build passes with no broken links or anchors.

Release coordination

The APIs are right; none of them has shipped. This is the one thing that
should gate the merge:

Merged to main Released
Java yes no - pom.xml is 1.3.10-SNAPSHOT, newest tag 1.3.7
Rust no no - crates.io questdb-rs 7.0.0 has no oidc feature
C / C++ no no - include/questdb/oidc.h absent from main
Python no no - PyPI is 5.0.0; CHANGELOG.rst says 5.0.1 (unreleased)
Enterprise server yes no - absent from tags 4.0.0 and 3.3.4

No client page states a minimum version, and the Rust snippet's
questdb-rs = { version = "7", features = ["oidc"] } is a copy-paste build
failure against the published crate today. It is not only the OIDC surface:
connect_with_token_provider, questdb_db_connect_ex and token_provider.rs
are all unmerged, so the rotating-provider pattern these sections recommend is
feature-branch work for Rust, C and C++.

Either hold this until the clients release, or add an explicit minimum-version
note to each of the five sections. The Cargo.toml needs fixing either way.

🤖 Generated with Claude Code

https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY

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) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

🚀 Build success!

Latest successful preview: https://preview-522--questdb-documentation.netlify.app/docs/

Commit SHA: ef66a91

📦 Build generates a preview & updates the link on each commit.

glasstiger and others added 28 commits August 21, 2026 17:10
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
`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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
`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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
"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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
glasstiger and others added 6 commits August 23, 2026 22:45
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@glasstiger glasstiger changed the title Document the missing OIDC configuration options docs: document the OIDC Device Authorization Flow, settings endpoint, and the missing configuration options Sep 2, 2026
glasstiger and others added 11 commits September 3, 2026 00:39
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111nSmXn2CmqkYMGobjvxgY
The page told every reader the prompt's text fields were display-safe. That
holds for Java, C and C++, which sanitize before the callback runs, but not for
Rust and Python, whose built-in renderers sanitize at display time and whose
custom renderers receive the provider's response verbatim. The page recommends a
custom renderer for headless and notebook use, so the assurance was given
exactly where it does not apply.

State the split, and name the helpers that appeared nowhere in the docs:
display_user_code() and display_verification_uri() in Rust,
sanitize_display_text in questdb.auth. Extend the browser target sentence past
C and C++, since Rust and Python expose one too.

questdb::error_kind::interaction_required does not compile: the enum is declared
in namespace questdb::oidc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUNkemSPGkrzov1yFrq6xt
Ten corrections, each checked against the client and server sources.

The browser table said only Java opens a browser. All five do by default, and
four expose an opt-out the page never named, so a headless host got a process
nobody can see and no documented switch.

The plaintext opt-out was described as a single flag. A client over plaintext
also refuses settings-sourced credential endpoints, so the flag needs an issuer
pin and explicit values alongside it. Say so, and say that the opt-out never
relaxes the Identity Provider legs.

issuer was framed as plumbing for a missing endpoint. /settings needs no
authentication, so pinning the issuer is what stops a tampered response steering
the sign-in, which is why it belongs against any untrusted server.

Java splits the settings across DiscoveryOptions and builder(), and the builder
demands the client ID and both endpoints together, so Java alone cannot combine
its own registration with discovered endpoints. The page claimed parity.

acl.oidc.groups.claim: the configured claim names apply in both flows, not just
the user info flow. The validator additionally requires literal sub and groups
in the payload. Conflating the two produced an instruction to reset
acl.oidc.sub.claim which contradicted the Entra ID walkthrough on the next page.

Also: rbac.md implied OIDC works on PGWire untouched, when it needs an opt-in
setting; steps 8 and 9 of the walkthrough described the user info flow as if it
were the only one; the PGWire section never said which token to send; the token
store, endpoint pinning and interaction-required recovery had no code in any
language; and the page linked to no client page at all, to no provider
walkthrough, and to no TLS setting despite requiring https.

Examples: ETH-USD to ETH-USDT per the demo dataset, the C partial gains the side
and amount columns the other four write, and Java assigns the timestamp
client-side like its siblings.

Retitle the configuration page to OIDC settings, so it no longer collides with
the guide in search results, and update the overview row to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUNkemSPGkrzov1yFrq6xt
The feature had five names across the surfaces a reader meets it on: the page
title, the sidebar, the guide's heading, the changelog and eleven link texts.
Settle on "OIDC device flow", which is what people search for and what the
sidebar already said, and pin the guide heading's old anchor so any external
link to it still resolves.

The sequence diagram drew discovery through QuestDB as the only path, though the
page documents pinning an issuer instead, and stopped at the first token though
the rest of the page is about what happens after it. Add the branch, run the
user's approval in parallel with the polling loop as RFC 8628 does, and note the
refresh. Verified with mermaid 11.9.0, the version the site ships.

The four client pages sent readers to #official-client-examples while promising
the sign-in prompt, the token store and explicit configuration, so the link
landed on the code the reader had just finished and left the promised material
several sections below. Point each at the section it names.

FileTokenStore::at_default_location() returns a std::io::Result, so the `?`
shown against a questdb::Result signature does not compile. Say what it returns,
and why it can fail at all.

Also: the token store's override is a dotted name, which most shells will not
export; the ROPC example indexed a settings key the same page documents as
conditional; a .env block was fenced as python and a server.conf block was not
fenced as anything, in a section that is now a link target; oidc.mdx imported
Tabs and TabItem without using either; and failover.md and dotnet.md carried
em-dashes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUNkemSPGkrzov1yFrq6xt
…homes

Four reference sections sat as H3s under "Official client examples", so the
client pages linking to them landed the reader on the code they had just read
with the promised material several sections below. Promote them to H2. Their
anchors are derived from the heading text, not its level, so every inbound link
still resolves.

Go, Node.js and .NET readers were sent here for the polling interval,
slow_down and the device code's expiry, which the page only named in passing
before saying the official clients handle them. Add a section that actually
sets out the two requests, the four token-endpoint error codes and what each
one means, and the refresh token, for anyone driving the grant themselves.

offline_access was hedged three ways: "if your provider requires" on the hub,
"most providers issue only when" on four client pages, each with its own
wording. State it once under acl.oidc.scope, where the setting lives, name
Entra ID as a provider that needs it, and say that adding it is harmless where
it is not needed. The hub and the client pages now defer to that.

Move the pointer to the client pages to the end of the page; it is a page-level
"where next" and had been sitting under Interaction-required errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUNkemSPGkrzov1yFrq6xt
The next Enterprise release checks exp when QuestDB validates a token itself,
so the warnings built around an expired token still being accepted no longer
hold. Move exp into the Checked column and rewrite the caution around the
caveat that survives: QuestDB never asks the provider about the token, so a
revoked token keeps working until it expires. The only lever is withdrawing the
signing key, which is what acl.oidc.public.keys.expiry already documents. The
acl.oidc.cache.ttl note, the Entra ID walkthrough and the changelog carried the
old claim too, and now agree. nbf and iss are still unchecked.

The Jupyter notebook section opened with JupyterHub and OAuthenticator, which
decide who may log in to JupyterHub and hand the notebook nothing to send to
QuestDB. Lead with the device flow, which is the answer to how a notebook
authenticates against QuestDB, and keep the JupyterHub point as the disclaimer
it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUNkemSPGkrzov1yFrq6xt
@glasstiger

Copy link
Copy Markdown
Contributor Author

Token validation: superseded by questdb-enterprise#1188

Review flagged that acl.oidc.groups.encoded.in.token's validation table claims
exp is checked, while jwt.rs set validate_exp = false. That is fixed on the
server side by questdb-enterprise#1188, which sets
set_required_spec_claims(&["sub", "aud", "exp"]) and validate_exp = true.

So the table on this branch is right for the release that carries #1188, and
wrong for every release before it. The same holds for acl.oidc.cache.ttl's
"that local check tests the token's expiry".

Two things still need a pass here, because #1188 changes more than exp.

1. The literal groups claim requirement is going away. #1188 drops
groups: Vec<String> from the native Claims struct, so
acl.oidc.groups.claim now selects the claim. Its upgrade note:

acl.oidc.groups.claim now selects the claim that carries group membership in
an ID token. Earlier releases required a claim literally named groups and
rejected the token before the setting was consulted, so a provider emitting
roles, or any other name, could not be used together with
acl.oidc.groups.encoded.in.token=true.

That contradicts two passages this PR adds to configuration/oidc.md:

  • "the JWT must carry claims named sub and groups literally, and groups
    must be an array of strings" - the array requirement also goes, since #1188
    tolerates a lone bare string, "some providers send a lone group that way"
  • "So set acl.oidc.groups.claim=groups, since that is the only name the
    validator accepts for the group memberships" - no longer true

2. The revocation caution is still not right after #1188. It says the token
"keeps working until its exp passes, and acl.oidc.cache.ttl does not shorten
that". Per #1188's upgrade note the cache extends acceptance past exp:

A token is also still accepted for a short window past exp: 60 seconds of
clock-skew leeway, plus up to acl.oidc.cache.ttl (30 seconds by default)
while an earlier validation of the same token remains cached.

It also notes the WebSocket connection is not torn down when its token expires,
which is worth a line on the QWP or device-flow page.

Since the docs now describe post-#1188 behaviour, both PRs should land together,
or these pages should say which version the behaviour applies from.

glasstiger and others added 2 commits September 4, 2026 16:20
The single oidc.mdx had grown to 1600 lines covering the architecture, the
sign-in walkthrough, client discovery, integration patterns, group mapping
and two provider setups. Each of those is now its own page under
security/oidc/, with the overview at the section root and a "where each
section moved" map for the old anchors. Inbound links across the
configuration, client and deployment docs are repointed at the new anchors.

Also corrects the group claim rules in the configuration reference. The
token validator requires sub, aud and exp and never looks at the group
memberships, so acl.oidc.groups.claim may name any claim the token carries
rather than being pinned to "groups", and a bare string is accepted where a
provider sends a lone group.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Cz9Rp9SBqLYSUsFkGFbGN
Same treatment as the OIDC page: rbac.md covered principals, authentication
and endpoint permissions, granting rules, the full permissions table and the
worked scenarios in one file. Each now has its own page under security/rbac/,
with the overview at the section root, and every inbound anchor across the
SQL reference, getting-started and operations docs repointed.

Two permission statements are corrected while passing through. REBASE WAL,
SWITCH COLD STORAGE ROLE and SET COLD STORAGE ROLE need the SYSTEM ADMIN
permission, which is grantable, rather than unspecified "database
administrator privileges" outside RBAC. all_permissions() lists every
permission the database supports, not the ones currently applied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Cz9Rp9SBqLYSUsFkGFbGN
@glasstiger glasstiger changed the title docs: document the OIDC Device Authorization Flow, settings endpoint, and the missing configuration options docs: rework the OIDC documentation and split the OIDC and RBAC guides into sections Sep 4, 2026
glasstiger and others added 7 commits September 4, 2026 17:31
The sign-in prompt table claimed Rust, C and C++ fail with an
interaction-required error when there is no terminal, and that Python fails
unless the kernel accepts stdin, summarised as a container with no TTY failing
outright in four of the five clients. Every one of those clients deliberately
has no TTY check, and says so: a missing terminal is not evidence of a missing
human, since a pipe into tee, a supervisor or an IDE capturing stderr all still
show the code to someone. A headless container therefore prompts to a stream
nobody reads and polls until the device code expires, which is the opposite of
what the page promised its own headline audience.

All five rows now say the call runs anyway. Python's one exception is stated for
what it is: positive evidence from a notebook executor issuing allow_stdin=false,
not a terminal check.

The fail-fast lever moves out of the custom-prompt table into its own, in the
direction that is useful. It read .interactive(true) for Rust and C++, which is
the default and so a no-op, and would have left the same wrong model in place
next to the corrected table above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Cz9Rp9SBqLYSUsFkGFbGN
Five examples across four pages either fail at runtime, fail to compile, or
describe syntax the server does not have.

The basic authentication example in the client integration guide calls
parse.urlencode() and json.loads() while importing neither, so it raises
NameError twice. Its own import list is self-contained, unlike the ROPC example
above it, which continues a block that does import both.

Both requests in that section posted to http://localhost:9000, one carrying an
OIDC bearer token and the other base64 SSO credentials, while the caution
further down the same page says both requests must go over https because a
tampered plaintext response can redirect the password grant. They now use the
https://questdb.example.com:9000 the rest of the page uses.

The Entra ID walkthrough creates the group extUsers and then grants twice to
groupName, which is never created, so running the three blocks in order fails on
the second. The placeholder table names go too, for the demo tables.

DROP TABLE ... CASCADE PERMISSIONS does not exist, and the default the note
described is the opposite of what happens: EntDdlListener drops a table's
permissions unconditionally, for tables, views, materialized views and live
views alike, so a drop-and-recreate silently loses every grant.

The Rust token store line used ? on at_default_location(), which returns
std::io::Result. There is no From<io::Error> for questdb::Error, so it does not
compile in the surrounding example's own signature. It now shows the map_err
form the upstream oidc_device_auth.rs example uses, and names the infallible
alternative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Cz9Rp9SBqLYSUsFkGFbGN
Turning the two guides into sidebar categories dropped both overview pages out
of the generated LLM index. The category branch of generate-llms-files.js emits
item.label as plain text and never reads item.link, so a category root reachable
only through link: produces a heading with no URL and no description. Before the
split both were type: "doc" entries and appeared as linked bullets.

That silently cost the index the RBAC quick start, the access control depth
tables, the only consolidated list of the ACL SQL statements, and the OIDC
architecture overview.

Repeating the index doc as an explicit Overview item is what the Enterprise
Kubernetes Operator category already does for the same reason. It also gives
each section a visible Overview row rather than leaving the root reachable only
by clicking the category header, and makes the sidebar_label in both index
frontmatters live instead of dead metadata.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Cz9Rp9SBqLYSUsFkGFbGN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant