feat: interactive OIDC device-flow authentication (questdb.auth)#133
feat: interactive OIDC device-flow authentication (questdb.auth)#133glasstiger wants to merge 22 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR introduces the Changesquestdb.auth OIDC Device Authorization Grant
Claude PR Review Skill
Sequence Diagram(s)sequenceDiagram
participant User
participant connect as questdb.auth.connect()
participant OidcDeviceAuth
participant resolve_config
participant QuestDB_Settings as QuestDB /settings
participant IdP
participant Renderer
participant TokenCache
participant QuestDB_Session as QuestDB session
User->>connect: connect(url, eager=True)
connect->>OidcDeviceAuth: from_questdb(url)
OidcDeviceAuth->>resolve_config: url, overrides
resolve_config->>QuestDB_Settings: GET /settings
QuestDB_Settings-->>resolve_config: acl.oidc.* config
resolve_config->>IdP: GET /.well-known/openid-configuration (if needed)
IdP-->>resolve_config: device_authorization_endpoint
resolve_config-->>OidcDeviceAuth: OidcConfig
connect->>QuestDB_Session: QuestDB(url, auth)
connect->>OidcDeviceAuth: token() [eager]
OidcDeviceAuth->>TokenCache: load(cache_key)
TokenCache-->>OidcDeviceAuth: miss
OidcDeviceAuth->>IdP: POST device_authorization_endpoint
IdP-->>OidcDeviceAuth: device_code, user_code
OidcDeviceAuth->>Renderer: on_prompt(resp)
Renderer-->>User: display verification URI + user code
loop Poll IdP
OidcDeviceAuth->>IdP: POST token_endpoint (device_code grant)
IdP-->>OidcDeviceAuth: authorization_pending / tokens
end
OidcDeviceAuth->>TokenCache: store(cache_key, TokenSet)
OidcDeviceAuth->>Renderer: on_success(identity, expires_in)
connect-->>User: QuestDB session
User->>QuestDB_Session: sql("SELECT ...") / sender() / sqlalchemy_engine()
🎯 4 (Complex) | ⏱️ ~75 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
src/questdb/auth/_questdb.py (1)
82-94: 💤 Low valueChain the import exception for better diagnostics.
When both
psycopgandpsycopg2fail to import, the raisedImportErrorloses the underlying cause. Chaining the exception preserves the diagnostic context.Suggested fix
def _pg_module(): try: import psycopg # type: ignore # psycopg v3 return psycopg except ImportError: pass try: import psycopg2 # type: ignore return psycopg2 - except ImportError: + except ImportError as e: raise ImportError( 'A PostgreSQL driver is required: install `psycopg` (v3) or ' - '`psycopg2-binary`.') + '`psycopg2-binary`.') from e🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/questdb/auth/_questdb.py` around lines 82 - 94, The second except ImportError block in the _pg_module() function should preserve the exception chain for better diagnostics. Capture the ImportError exception in the second except block using `as e` syntax, then use `raise ... from e` syntax when raising the new ImportError to chain the exception, which preserves the underlying cause and provides better debugging context.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/skills/review-pr/SKILL.md:
- Around line 157-162: The Step 3 preamble at lines 157-162 states that every
agent receives the full change-surface map, but Agent 10's input contract at
lines 231-240 explicitly limits it to only the PR diff and changed file names.
Add an explicit exception clause in the Step 3 preamble section to clarify that
Agent 10 does not receive the full change-surface map and instead receives only
the diff and changed file names, ensuring consistency between the general rule
and the specific Agent 10 behavior documented at lines 231-240.
- Around line 39-42: The table at lines 39-42 states that level 0 (default)
skips Step 2.5, but there is a later reference (around lines 144-155) that marks
Step 2.5e as mandatory at every level, creating a contradiction. Fix this
inconsistency by clarifying the level 0 description in the table: either modify
the "Skip Step 2.5" statement to specify that Step 2.5e is an exception and is
still performed, or restructure it to list exactly which substeps (2.5a, 2.5b,
2.5c, 2.5d, 2.5e) are executed or skipped. Ensure the description at level 0 and
the mandatory requirement stated at lines 144-155 are aligned so the default
path is no longer self-contradictory.
In `@docs/auth.rst`:
- Around line 65-76: The code example in the documentation is missing the import
statement for TimestampNanos, which is used in the sender.row() method call. Add
an import statement at the top of the code snippet to include TimestampNanos
from the questdb.ingress module (or appropriate module) so that the example can
be executed successfully when copied verbatim.
In `@src/questdb/auth/__init__.py`:
- Around line 75-92: The `__all__` list in the module needs to be sorted
alphabetically to comply with Ruff's RUF022 rule and maintain consistency in the
public API exports. Reorder all the items in the `__all__` list (which includes
'connect', 'QuestDB', 'OidcDeviceAuth', 'OidcConfig', 'TokenCache', 'TokenSet',
'MemoryCache', 'FileCache', 'NullCache', and the various OidcError types) in
alphabetical order while ensuring all items remain in the list.
In `@src/questdb/auth/_http.py`:
- Around line 123-129: The `_opener()` function doesn't receive the `insecure`
parameter, so it cannot enforce HTTPS-only redirect policies. To fix this, add
an `insecure` parameter to the `_opener()` function signature. When
`insecure=False`, create a custom redirect handler that validates redirects do
not downgrade from HTTPS to HTTP for non-loopback hosts (similar to the
validation in `_require_secure()`), and pass this handler to
`urllib.request.build_opener()` instead of using the default. When
`insecure=True`, use urllib's default redirect handling. Update all callers of
`_opener()` to pass the `insecure` parameter value.
In `@test/test_auth.py`:
- Around line 600-605: The timed joins with a 10-second timeout do not verify
that threads actually completed, which can cause non-daemon threads to remain
running and hang the test process if a regression causes a deadlock. After
calling t.join(10) on each thread in both the loop at lines 600-605 and at lines
828-835, add a verification check that confirms each thread has terminated by
asserting that t.is_alive() returns False, or raise an exception if any thread
is still alive after the timeout. This ensures the test fails cleanly rather
than leaving hanging threads.
---
Nitpick comments:
In `@src/questdb/auth/_questdb.py`:
- Around line 82-94: The second except ImportError block in the _pg_module()
function should preserve the exception chain for better diagnostics. Capture the
ImportError exception in the second except block using `as e` syntax, then use
`raise ... from e` syntax when raising the new ImportError to chain the
exception, which preserves the underlying cause and provides better debugging
context.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bf4af383-9022-4ae5-b189-73691f4ccd00
📒 Files selected for processing (18)
.claude/skills/review-pr/SKILL.mdCHANGELOG.rstdocs/api.rstdocs/auth.rstdocs/index.rstdocs/installation.rstexamples/oidc_device_auth.pysetup.pysrc/questdb/auth/__init__.pysrc/questdb/auth/_cache.pysrc/questdb/auth/_device.pysrc/questdb/auth/_discovery.pysrc/questdb/auth/_errors.pysrc/questdb/auth/_http.pysrc/questdb/auth/_questdb.pysrc/questdb/auth/_render.pytest/test.pytest/test_auth.py
The fastparquet -> pyarrow parquet roundtrip decays the categorical
column to a plain string column. On pandas >= 3 that reads back as the
new default string dtype (StringDtype(na_value=nan)) rather than object,
so the hardcoded np.dtype('O') in fallback_exp_dtypes no longer matched
and the assertion failed.
Derive the expected dtype from pd.Series(['x']).dtype instead of
hardcoding it: this is object on pandas < 3 and the new string dtype on
pandas >= 3, matching exactly what pyarrow's read_parquet produces, so
the test is version-agnostic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pandas 3 ships no 32-bit wheels. On win32 Python 3.11+ the pandas>=3 install was silently swallowed, fastparquet then pulled in a numpy-1-built pandas 2.0.3 alongside numpy 2, and importing pandas crashed at runtime. test.py tolerated the failed import and silently skipped every pandas test (skip count 39 vs 32), while the 64-bit-only import sanity check never fired to catch it. Gate should_use_pandas3() on a 64-bit interpreter so 32-bit targets stay on the consistent pandas 2 / numpy 1 stack and actually exercise the dataframe tests again. 64-bit targets keep testing pandas 3 unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mock HTTP server only caught BrokenPipeError, but on Windows an abrupt client disconnect raises ConnectionAbortedError/ConnectionResetError -- siblings of BrokenPipeError under the common base ConnectionError, not subclasses of it. The timeout, min-throughput, and retry tests disconnect mid-request on purpose, so these slipped past the handler and the stdlib dumped tracebacks to stderr. The tests still passed, but the CI logs looked broken. Broaden the handler except clauses to ConnectionError, and override HTTPServer.handle_error to swallow connection errors that surface in the stdlib keep-alive readline of the next request line -- outside any request handler's try/except. Real errors are still reported. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The windows-2025 image injects a new readonly agent variable, AZP_ENHANCED_WORKER_CRASH_HANDLING. The Windows "Build wheels" step re-exports the vcvars environment via ##vso[task.setvariable ...], and attempting to set this readonly var made the agent emit an ##[error], marking the task failed even though all wheels built and tests passed. Add AZP_ENHANCED to the exclusion regex (prefix match also covers any future AZP_ENHANCED_* vars) in both windows_i686 and windows_x86_64. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review fixes on the OIDC device-flow auth module, plus a simplification that removes the file cache entirely. Hardening: - device-flow poll gates success on _has_required_token (id_token in groups mode, else access_token) instead of always access_token: a completed grant missing the required kind now fails once with a clear error instead of caching an unusable token or discarding a usable id_token. - QuestDB.sql() guards the 2xx path against non-JSON / non-dict bodies, raising OidcError instead of a raw JSONDecodeError / AttributeError. - discovery requires an explicit issuer= (or discovery_url=) before the IdP .well-known fallback; the discovery origin is never derived from a server-supplied token endpoint, so a tampered /settings can't redirect the device-code / refresh-token POSTs. - PG-wire / ILP adapters bracket IPv6 literals in the ILP addr= and raise a clear error on a host-less URL instead of passing None to the driver. - validate_endpoint_origins and cache-key normalization raise OidcConfigError (not a bare ValueError) on a malformed port, via a shared safe_urlparse helper. - the example imports questdb.ingress lazily, so it loads on the pure-Python path with no compiled extension. Simplification: - drop FileCache and its cross-process locking + at-rest refresh token; MemoryCache (process-global, survives notebook cell re-runs) is the only persistent backend, with NullCache for cache=None. This also removes the Windows msvcrt-lock no-op and corrupt-file edge cases entirely. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…file The linux-qdb-master job's "Compile QuestDB master" step failed because questdb master depends on a -SNAPSHOT java-questdb-client that is not published to Maven Central. Add a detect-local-client step template (adapted from questdb/questdb's ci/templates/detect-local-client.yml) that reads questdb.client.version from the cloned core/pom.xml: for a -SNAPSHOT client it inits the java-questdb-client submodule and builds it via the `local-client` Maven profile; for a released client it resolves from Maven Central. The "Compile QuestDB master" step appends the resulting $(CLIENT_PROFILE). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QuestDB master bumped its build to Java 25 (javac.target=25). Its maven-enforcer requireJavaVersion reads ${java.enforce.version}, which is only set by the JDK-activated 'java25+' profile (<jdk>(24,)</jdk>). Building the linux-qdb-master leg with JDK 17 left that property empty, so the enforcer failed with 'JDK version can't be empty' before compilation.
Point the 'Compile QuestDB master' Maven task at $(JAVA_HOME_25_X64) via jdkVersionOption: path, and run 'Test vs master' on the same JDK 25 so the freshly compiled Java 25 bytecode can run. 'Test vs released' stays on JDK 17. Both ubuntu-latest and windows-2025 images preinstall Temurin 25 (JAVA_HOME_25_X64).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Maven@3 task crashes while parsing JDK 25 ('Cannot read properties of null (reading 'major')') — its JDK selection tops out at 21 and its Node-side version detector returns null for 25. Replace the task with a bash step that exports JAVA_HOME=$(JAVA_HOME_25_X64) and runs mvn directly, mirroring the task defaults (questdb/pom.xml, goal 'package', same -DskipTests -Pbuild-web-console $(CLIENT_PROFILE) options).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QuestDB master runs as the io.questdb JPMS module and now uses jdk.internal.vm.ContinuationScope, so on JDK 25 the server dies at startup with IllegalAccessError (java.base does not export jdk.internal.vm to io.questdb), plus Unsafe/native-access warnings. The test fixture launches questdb.jar directly rather than via questdb.sh, so the access flags questdb.sh normally supplies are absent. Set JDK_JAVA_OPTIONS on the 'Test vs master' step with the exact module access flags from questdb.sh (all targeting io.questdb). Scoped to that step, so the JDK 17 'Test vs released' run is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When QuestDB is reached over plaintext http to a non-loopback host (only possible with insecure=True), its /settings response is MITM-able. The issuer-pin requirement previously fired only when an IdP endpoint was missing (the discovery path). A tampered /settings advertising BOTH the token and device-authorization endpoints at one attacker origin skipped that path: the co-location check passed trivially (same origin) and the issuer-pin check was vacuous (no issuer), so the device code and the long-lived refresh token were POSTed to the attacker. Require the same out-of-band pin (issuer= / discovery_url=) before trusting /settings-supplied credential endpoints fetched over such an untrusted channel. Endpoints the caller passed explicitly, and endpoints from an authenticated (https / loopback) /settings, are unaffected, so the https happy path and local-dev loopback are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The device-authorization response's expires_in / interval were trusted
verbatim, so a hostile or buggy IdP could break or stall the poll loop —
which runs under the acquisition lock, so a stall freezes every other
thread needing a token on that instance:
* expires_in <= 0 set the deadline to "now", timing the flow out before
its first poll even though the user could still authorize;
* an unbounded interval (or repeated slow_down) produced a single
enormous sleep() holding the lock.
Clamp both: expires_in <= 0 -> default, capped at a max lifetime; interval
to [1s, 60s] (including after slow_down); and never sleep past the
deadline. RFC-typical values (interval=5, expires_in=600) are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Several malformed-input paths escaped the package's typed-error contract
(callers catch OidcError) with a bare ValueError / AttributeError /
http.client.InvalidURL:
* a non-string OIDC endpoint in /settings -> AttributeError from
.startswith(); now treated as absent so resolution raises a clear
OidcConfigError;
* a /exec "columns" entry that isn't an object -> AttributeError from
.get(); now raises OidcError;
* a malformed port in the QuestDB URL -> bare ValueError when an adapter
read .port; QuestDB now validates it at construction via safe_urlparse;
* the same malformed port reaching the /settings or discovery fetch ->
http.client.InvalidURL; request() now wraps InvalidURL as
OidcConfigError and any other HTTPException as OidcNetworkError, so the
single HTTP choke point never leaks a raw http.client exception.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plain-text terminal prompt wrote untrusted device-authorization response fields — verification_uri, user_code, the IdP error_description and the JWT-derived identity — verbatim to the TTY. A hostile or MITM'd response could embed ANSI escape sequences (cursor moves, screen clears) to spoof the sign-in prompt or hide the real verification URL. Strip C0/C1 control characters (incl. ESC) from those untrusted strings in format_prompt() and TerminalRenderer.on_success/on_failure before they reach the stream. The Jupyter renderer already html-escapes its output, and the QR path encodes the URL as image data rather than terminal text, so neither needed changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* CHANGELOG: drop the stale "optional on-disk cache" claim (the FileCache backend was removed; tokens are never written to disk) and note the python_requires bump to 3.10. * docs/auth.rst: import TimestampNanos in the integrated-session snippet so it runs as written. * docs/api.rst: document TokenCache, TokenSet, MemoryCache and NullCache, which are exported in __all__ but were missing from the reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lock-free fast path in OidcDeviceAuth reads a published TokenSet without holding a lock, which is only safe because its fields never change after construction — an invariant previously kept by convention alone. Mark TokenSet frozen so any future in-place mutation fails loudly instead of introducing a torn read, and convert the one such mutation (the refresh carry-forward in _refresh) to dataclasses.replace(). Also keep the access/id/refresh tokens out of repr() so a TokenSet that lands in a log line or traceback cannot leak credentials. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reorder the export list using Ruff's isort-style ordering (CamelCase names first, natural-sorted, then lowercase 'connect' last). The public API is unchanged; this only resolves the RUF022 lint warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The level table said level 0 skips all of Step 2.5, but Step 2.5e is documented as running at every level — a self-contradictory default path. Clarify that levels 0 and 1 skip Steps 2.5a-d while still running Step 2.5e (build & binding profile), aligning the table with the 'runs at every level' rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Step 3 preamble and Step 2.5 stated that every Step 3 agent receives the change-surface map and build/binding profile facts, but Agent 10 (the fresh-context adversarial agent) is documented as receiving only the diff and changed file names. Carve Agent 10 out of all three general-rule statements (Steps 2.5, 2.5e, and the Step 3 input contract) so they no longer contradict Agent 10's own section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QuestDB /settings nests server-authoritative values under a top-level "config" object alongside a user-writable "preferences" sibling (the web console persists UI prefs there via PUT /settings). Discovery now reads only "config" and refuses to fall back to the top level of a structured response, so a user who can write a preference cannot smuggle an acl.oidc.* key (e.g. a redirected token endpoint that points the device code / refresh token at an attacker) into the resolved OIDC config. Genuinely flat legacy responses are still tolerated at the top level. Ports the trust model from the Java client (java-questdb-client#52). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QuestDB._require_host() passed the URL hostname unsanitized into the ILP
conf string sender() builds (addr=host:port;). urlparse keeps ';' and '='
in .hostname, so a crafted or tampered URL such as
"https://host;tls_verify=unsafe_off;x=" injected extra conf params —
silently disabling the sender's TLS certificate verification (and exposing
the bearer token to a MITM), or e.g. auto_flush=off for data loss.
Reject ';', '=', whitespace and control characters in the resolved host
(':' stays allowed for IPv6 literals) at the single chokepoint shared by
sender()/psycopg()/sqlalchemy_engine(). Add a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
New
questdb.authmodule that lets you sign in interactively to OIDC-securedQuestDB Enterprise from Python — including from remote kernels (JupyterHub,
SageMaker, Colab, VS Code-remote, containers) that have no local browser.
It runs the OAuth 2.0 Device Authorization Grant (RFC 8628)
entirely client-side: you authorize in any browser (laptop or phone), while the
kernel only makes outbound calls to your identity provider (IdP). The resulting
token is presented to QuestDB over the auth paths it already supports — HTTP
Authorization: Beareror PG-wire_sso— so no server change is required.Pure Python, built on the standard library (
urllib);questdb.ingressisnever imported at module load, and all optional deps are lazy.
Two ways to use it
Just the token — no extra dependencies; works with PG-wire, HTTP, or any client:
The integrated session — query to a DataFrame and feed adapters:
On first use you get a sign-in prompt (clickable link in Jupyter, plain text on
a terminal). Re-running is silent: the token is cached and refreshed silently as
it nears expiry.
Highlights
/settingsendpoint (acl.oidc.*),falling back to the IdP
.well-known/openid-configuration. Anything passedexplicitly overrides discovery; discovery can also be skipped entirely.
(
groupsEncodedInToken ? id_token : access_token), requesting theopenidscope automatically when the id_token is sent. A completed grant that is
missing the required token kind fails once with a clear error rather than
caching an unusable token.
refresh_token; re-prompts only when the refresh token is missing/rejected,while a transient network error is surfaced (not re-prompted). A lock
serializes refresh so parallel cells/threads don't double-prompt. Cache
backends:
"memory"(default — process-global, survives notebook cellre-runs),
None(no cache, prompt every time), or a customTokenCacheinstance. Tokens are never written to disk.
/exec), SQLAlchemy, psycopg/psycopg2,and the ingestion
Sender.OidcInteractionRequiredinstead ofhanging under papermill / cron / CI.
qrcode) with aplain-text terminal fallback.
OidcErrorsubclass(
OidcConfigError,OidcNetworkError,OidcInteractionRequired,OidcDeviceFlowError,OidcTimeoutError,OidcAuthError) — malformed config,HTTP/URL errors and bad server payloads are mapped to these rather than leaking
raw
ValueError/AttributeError/http.clientexceptions.Security
httpsis required; plaintext is allowed only to a loopback address.insecure=Truepermits plaintext to a non-loopback QuestDB host only — itnever downgrades the IdP (so the device code and refresh token are never
sent in cleartext) and never disables certificate verification.
origin, and HTTP redirects are refused — urllib doesn't strip the
Authorizationheader across a cross-origin30x, and the origin check onlysees the pre-redirect URL, so a silently-followed redirect could leak the token
or refresh token. A
30xon those endpoints surfaces as an error instead.issuer=/discovery_url=to pin the IdP. Thediscovery origin is never derived from a server-supplied token endpoint, so a
tampered
/settingscan't redirect the device-code / refresh-token POSTs. Thepin is required before trusting credential endpoints that arrive over an
untrusted plaintext
/settings(only reachable withinsecure=True)./settingsconfig is trusted — discovery readsthe nested
configobject and ignores the user-writablepreferencessibling(which the web console persists via
PUT /settings), so a user who can write apreference can't smuggle an
acl.oidc.*override (e.g. a redirected tokenendpoint) into the resolved config.
expires_in/intervalareclamped (sane lifetime cap, interval in
[1s, 60s], never sleeps past thedeadline), so a hostile or buggy IdP can't time the flow out before the first
poll or stall the loop while it holds the acquisition lock.
stripped from untrusted device-response fields (
verification_uri,user_code, IdPerror_description, JWT-derived identity) before they reachthe TTY, so a MITM'd response can't inject ANSI escapes to spoof the prompt.
The Jupyter renderer already HTML-escapes its output.
TokenSetis immutable (frozen) and keepsthe access/id/refresh tokens out of its
repr, so a token can't leak into alog line or traceback. Adapters avoid logging the token / PG DSN.
HTTPS_PROXY,REQUESTS_CA_BUNDLE,SSL_CERT_FILE, andca_bundle=.Dependencies & Python support
token()/headers()need nothing beyond the standard library.pandas,sqlalchemy,psycopg/psycopg2,qrcodeandIPythonare importedlazily, only when used. Minimum supported Python is raised to 3.10.
Docs, examples & tests
docs/auth.rst(:ref:oidc_auth), API reference indocs/api.rst(incl.
TokenCache,TokenSet,MemoryCache,NullCache), index/installationupdates, and a
CHANGELOG.rstentry.examples/oidc_device_auth.py.test/test_auth.py— pure-Python (no compiled extension required), wired intothe suite via
test/test.py. Covers the device flow, refresh, non-interactivecontexts, discovery, REST/PG adapters, concurrency, endpoint validation,
cache-key derivation, and transport/renderer security.
Also in this PR (CI & build robustness)
Bundled fixes to keep the matrix green, independent of the auth feature:
test_parquet_roundtripinstead of hardcoding
object(pandas 3 reads back the new string dtype); keep32-bit wheel targets on the consistent pandas 2 / numpy 1 stack so the
dataframe tests actually run there.
-SNAPSHOTjava client via thelocal-clientMaven profile (newci/templates/detect-local-client.yml), invoke Maven directly (the Maven taskcan't parse JDK 25), and pass the JPMS module-access flags the server needs.
(
ConnectionErrorfamily), and skip the readonlyAZP_ENHANCED*agent var inthe wheel-build env re-export.
review-prreview skill (.claude/skills/review-pr/).Summary by CodeRabbit
New Features
Documentation
Compatibility
Tests