feat(rust): OAuth token primitives, OIDC discovery, cache and token store#320
Conversation
62e94c8 to
e0a0fdc
Compare
fe4a411 to
1651567
Compare
Range-diff: stack/oauth-u2m-m2m-design (fe4a411 -> 1651567)
Reproduce locally: |
f0a904f to
bd474c1
Compare
rust/src/auth/oauth/token_store.rs
Outdated
| fn wait_for_refresh(&self) -> Result<OAuthToken> { | ||
| // Spin-wait until the refresh completes | ||
| while self.refreshing.load(Ordering::SeqCst) { | ||
| std::thread::yield_now(); |
There was a problem hiding this comment.
yield_now will consume cpu, can we add sleep instead?
check parking_lot::Condvar
There was a problem hiding this comment.
Agreed — replaced yield_now() with sleep(1ms) to avoid busy-waiting. A Condvar would be cleaner but more invasive for the rare case where we actually hit the wait path (only when multiple threads race on an expired token simultaneously).
rust/src/auth/oauth/token_store.rs
Outdated
| } | ||
|
|
||
| // Release the refresh lock | ||
| self.refreshing.store(false, Ordering::SeqCst); |
There was a problem hiding this comment.
we should also set refresh to false for panic cases
There was a problem hiding this comment.
Good catch — added a RefreshGuard RAII type that resets refreshing to false on Drop. Used in both blocking_refresh and spawn_background_refresh, so the flag is always cleared even if refresh_fn panics.
| .and_then(|b| b.as_bytes()) | ||
| .map(|b| b.to_vec()) | ||
| } else { | ||
| None |
There was a problem hiding this comment.
you don't need request body ?
There was a problem hiding this comment.
No request body needed — OIDC discovery is a standard GET request to the well-known endpoint. The server returns JSON with the authorization and token endpoints.
|
|
||
| // Create cache directory if it doesn't exist | ||
| if let Some(parent) = cache_path.parent() { | ||
| if let Err(e) = fs::create_dir_all(parent) { |
There was a problem hiding this comment.
this usually uses 755 permission, through which others can also get access on the file
There was a problem hiding this comment.
Good point — tightened directory permissions to 0o700 (owner only) after create_dir_all. The files themselves are already 0o600.
| /// ``` | ||
| pub async fn discover(host: &str, http_client: &Arc<DatabricksHttpClient>) -> Result<Self> { | ||
| // Construct the well-known endpoint URL | ||
| let discovery_url = format!("{}/oidc/.well-known/oauth-authorization-server", host); |
There was a problem hiding this comment.
if host has trailing '/', the discovery url will have 2 '/'s
There was a problem hiding this comment.
Good catch — added host.trim_end_matches('/') before building the discovery URL.
## 🥞 Stacked PR Use this [link](https://github.com/adbc-drivers/databricks/pull/319/files) to review incremental changes. - [**stack/oauth-u2m-m2m-design**](#319) [[Files changed](https://github.com/adbc-drivers/databricks/pull/319/files)] - [stack/pr-oauth-foundation](#320) [[Files changed](https://github.com/adbc-drivers/databricks/pull/320/files/250ff3d91c3001f671f08084f68e949e556bc5d2..bd474c189621aa70c1f14e97c32d64605275e07d)] - [stack/pr-database-config](#321) [[Files changed](https://github.com/adbc-drivers/databricks/pull/321/files/bd474c189621aa70c1f14e97c32d64605275e07d..296931cd396d82dccb1b548a51f6b9d31be3683e)] - [stack/pr-u2m-provider](#322) [[Files changed](https://github.com/adbc-drivers/databricks/pull/322/files/296931cd396d82dccb1b548a51f6b9d31be3683e..c96689981e79c04f43e8251f2cbd5690371dfca5)] - [stack/pr-integration-tests](#323) [[Files changed](https://github.com/adbc-drivers/databricks/pull/323/files/c96689981e79c04f43e8251f2cbd5690371dfca5..83d639337ca30688abb7bdba85aa16426d76eb31)] - [stack/pr-final-validation](#324) [[Files changed](https://github.com/adbc-drivers/databricks/pull/324/files/83d639337ca30688abb7bdba85aa16426d76eb31..e2cd82bf1e9510169735774784591074f30351d3)] --------- ## Summary - Design document for adding OAuth 2.0 authentication to the Rust ADBC driver covering both U2M (Authorization Code + PKCE) and M2M (Client Credentials) flows - Sprint plan breaking the implementation into 3 tasks: foundation + HTTP client changes, M2M provider, U2M provider - Uses the `oauth2` crate for protocol-level operations, unified `DatabricksHttpClient` with two-phase `OnceLock` init, and ODBC-aligned numeric config values (`AuthMech`/`Auth_Flow`) ## Key decisions and alternatives considered - **`oauth2` crate adoption** over hand-rolling OAuth protocol (eliminates ~200 lines of boilerplate, handles PKCE/token exchange/refresh) - **Unified HTTP client** (`DatabricksHttpClient` with `OnceLock`) over separate `reqwest::Client` for token calls (shared retry logic, connection pooling) - **ODBC-aligned numeric config** (`mechanism=0/11`, `flow=0/1/2`) over string-based or auto-detection (explicit, predictable, matches ODBC driver) - **Separate U2M/M2M providers** over single OAuthProvider (different flows, refresh strategies, caching needs) - **Separate token cache** (`~/.config/databricks-adbc/oauth/`) over sharing Python SDK cache (fragile cross-SDK compatibility) ## Areas needing specific review focus - Two-phase HTTP client initialization pattern (OnceLock for auth provider) — is this the right approach for breaking the circular dependency? - Token refresh state machine (FRESH/STALE/EXPIRED) — are the thresholds (40s expiry buffer, min(TTL*0.5, 20min) stale) appropriate? - Config option naming (`databricks.auth.mechanism`, `databricks.auth.flow`) — alignment with ODBC driver - Sprint plan task breakdown — is the scope realistic for 2 weeks? --- *Replaces #318 (closed — converted to stacked branch)* 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
…-1.1-oauth-token-struct
…D: task-1.4-token-store
d601309 to
0968101
Compare
15ef8c4 to
7fd8031
Compare
…task-1.5-oauth-module-setup
7fd8031 to
78b9ec8
Compare
🥞 Stacked PR
Use this link to review incremental changes.
Summary
Core OAuth token infrastructure used by both U2M and M2M flows:
OAuthToken(token.rs) — token struct with expiry tracking (40s buffer), stale detection (min(TTL*0.5, 20min)), and serde supportTokenStore(token_store.rs) — thread-safe token lifecycle state machine (Empty → Fresh → Stale → Expired) withRwLock+AtomicBoolcoordination, background refresh viatokio::task::spawn_blocking, panic-safeRefreshGuardoidc.rs) — fetches authorization and token endpoints from/.well-known/oauth-authorization-serverTokenCache(cache.rs) — file-based U2M token persistence at~/.config/databricks-adbc/oauth/with SHA-256 hashed filenames and0o600/0o700permissionsDatabricksHttpClient(http.rs) — extended withOnceLock-based auth provider for two-phase initialization andexecute_without_auth()for token endpoint callsoauth2,sha2,dirs,serde,opencratesabout.tomlfor transitive depsKey files
src/auth/oauth/token.rs—OAuthTokenstructsrc/auth/oauth/token_store.rs— token lifecycle state machinesrc/auth/oauth/oidc.rs— OIDC endpoint discoverysrc/auth/oauth/cache.rs— file-based token cachesrc/client/http.rs— HTTP client auth provider integrationThis pull request was AI-assisted by Isaac.