Skip to content

Commit 250ff3d

Browse files
docs: add OAuth implementation sprint plan
3-task breakdown covering foundation + HTTP client changes, M2M provider, and U2M provider with full test coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f478b62 commit 250ff3d

File tree

2 files changed

+204
-99
lines changed

2 files changed

+204
-99
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<!--
2+
Copyright (c) 2025 ADBC Drivers Contributors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
17+
# Sprint Plan: OAuth U2M + M2M Implementation
18+
19+
**Sprint dates:** 2026-03-07 to 2026-03-20
20+
**Sprint goal:** Implement complete OAuth 2.0 authentication (U2M + M2M) for the Rust ADBC driver as described in the [OAuth design doc](oauth-u2m-m2m-design.md).
21+
22+
---
23+
24+
## Story
25+
26+
**Title:** Implement OAuth U2M and M2M authentication
27+
28+
**Description:**
29+
Add OAuth 2.0 support to the Rust ADBC driver, covering:
30+
- M2M (Client Credentials) flow for service principal authentication
31+
- U2M (Authorization Code + PKCE) flow for interactive browser-based login
32+
- Shared infrastructure: OIDC discovery, token lifecycle management, file-based token caching
33+
- Integration with `Database` config and `DatabricksHttpClient` (two-phase init via `OnceLock`)
34+
- ODBC-aligned numeric config values (`AuthMech`/`Auth_Flow` scheme)
35+
36+
**Acceptance Criteria:**
37+
- [ ] M2M flow works end-to-end: `Database` config -> `new_connection()` -> `get_auth_header()` returns valid Bearer token from client credentials exchange
38+
- [ ] U2M flow works end-to-end: browser launch -> callback server captures code -> PKCE token exchange -> cached token reused on subsequent connections
39+
- [ ] Token refresh state machine works: FRESH (no-op) -> STALE (background refresh) -> EXPIRED (blocking refresh)
40+
- [ ] File-based token cache persists U2M tokens across connections with correct permissions (0o600)
41+
- [ ] `DatabricksHttpClient` supports two-phase init: created without auth, auth set later via `OnceLock`
42+
- [ ] All config options from the design doc are parseable via `set_option()`
43+
- [ ] Invalid config combinations fail with clear error messages at `new_connection()`
44+
- [ ] `cargo fmt`, `cargo clippy -- -D warnings`, `cargo test` all pass
45+
46+
---
47+
48+
## Sub-Tasks
49+
50+
### Task 1: Foundation + HTTP Client Changes
51+
52+
**Scope:** Shared OAuth infrastructure and modifications to existing code to support two-phase auth initialization.
53+
54+
**Files to create:**
55+
- `src/auth/oauth/mod.rs` — module root, re-exports
56+
- `src/auth/oauth/token.rs``OAuthToken` struct with `is_expired()`, `is_stale()`, JSON serialization
57+
- `src/auth/oauth/oidc.rs``OidcEndpoints` struct, `discover()` function hitting `{host}/oidc/.well-known/oauth-authorization-server`
58+
- `src/auth/oauth/cache.rs``TokenCache` with `load()`/`save()`, SHA-256 hashed filenames, 0o600 permissions
59+
- `src/auth/oauth/token_store.rs``TokenStore` with `RwLock<Option<OAuthToken>>`, `AtomicBool` for refresh coordination, FRESH/STALE/EXPIRED state machine
60+
61+
**Files to modify:**
62+
- `Cargo.toml` — add `oauth2 = "5"`, `sha2 = "0.10"`, `open = "5"`, `dirs = "5"`
63+
- `src/client/http.rs` — change `auth_provider` from `Arc<dyn AuthProvider>` to `OnceLock<Arc<dyn AuthProvider>>`, add `set_auth_provider()`, update `execute()` to read from `OnceLock`
64+
- `src/database.rs` — add `AuthMechanism`/`AuthFlow` enums (with `TryFrom<i64>`), new config fields (`auth_mechanism`, `auth_flow`, `auth_client_id`, `auth_client_secret`, `auth_scopes`, `auth_token_endpoint`, `auth_redirect_port`), `set_option()`/`get_option_string()` for all new keys
65+
- `src/auth/mod.rs` — add `pub mod oauth;`, remove old `pub use oauth::OAuthCredentials`, delete `src/auth/oauth.rs` (replaced by directory module)
66+
67+
**Tests:**
68+
- `token.rs`: `test_token_fresh_not_expired`, `test_token_stale_threshold`, `test_token_expired_within_buffer`, `test_token_serialization_roundtrip`
69+
- `oidc.rs`: `test_discover_workspace_endpoints`, `test_discover_invalid_response`, `test_discover_http_error`
70+
- `cache.rs`: `test_cache_key_deterministic`, `test_cache_save_load_roundtrip`, `test_cache_missing_file`, `test_cache_file_permissions`, `test_cache_corrupted_file`
71+
- `token_store.rs`: `test_store_fresh_token_no_refresh`, `test_store_expired_triggers_blocking_refresh`, `test_store_concurrent_refresh_single_fetch`, `test_store_stale_returns_current_token`
72+
- `http.rs`: `test_execute_without_auth_works_before_auth_set`, `test_execute_fails_before_auth_set`, `test_execute_succeeds_after_auth_set`, `test_set_auth_provider_twice_panics_or_errors`
73+
- `database.rs`: `test_set_auth_mechanism_valid`, `test_set_auth_mechanism_invalid`, `test_set_auth_flow_valid`, `test_set_auth_flow_invalid`, `test_new_connection_missing_mechanism`, `test_new_connection_oauth_missing_flow`, `test_new_connection_pat_missing_token`
74+
75+
**Definition of done:** All foundation modules compile, unit tests pass, `DatabricksHttpClient` two-phase init works, database config parsing works for all auth options.
76+
77+
---
78+
79+
### Task 2: M2M Provider (Client Credentials)
80+
81+
**Scope:** Implement the M2M OAuth flow using `oauth2::BasicClient` for client credentials exchange.
82+
83+
**Files to create:**
84+
- `src/auth/oauth/m2m.rs``ClientCredentialsProvider` implementing `AuthProvider`
85+
86+
**Files to modify:**
87+
- `src/auth/oauth/mod.rs` — add `pub mod m2m;`, re-export `ClientCredentialsProvider`
88+
- `src/auth/mod.rs` — re-export `ClientCredentialsProvider`
89+
- `src/database.rs` — wire `AuthFlow::ClientCredentials` match arm in `new_connection()` to create `ClientCredentialsProvider`
90+
91+
**Implementation details:**
92+
- Construct `oauth2::BasicClient` from OIDC-discovered endpoints
93+
- Implement `oauth2` HTTP adapter that routes through `DatabricksHttpClient::execute_without_auth()`
94+
- Use `TokenStore` for token lifecycle (no disk cache for M2M)
95+
- `get_auth_header()``TokenStore::get_or_refresh()``client.exchange_client_credentials()` if needed
96+
97+
**Tests:**
98+
- `m2m.rs`: `test_m2m_token_exchange`, `test_m2m_auto_refresh`, `test_m2m_oidc_discovery`
99+
- Wiremock integration (`tests/`): `test_m2m_full_flow_discovery_and_token_exchange`, `test_m2m_token_refresh_on_expiry`, `test_m2m_discovery_failure_propagates`
100+
- Database validation: `test_new_connection_client_credentials_missing_secret`
101+
102+
**Definition of done:** M2M flow works end-to-end from `Database` config through `new_connection()` to `get_auth_header()`. Wiremock tests verify the full OIDC discovery -> token exchange -> refresh cycle.
103+
104+
---
105+
106+
### Task 3: U2M Provider (Authorization Code + PKCE)
107+
108+
**Scope:** Implement the U2M OAuth flow with browser-based login, PKCE, callback server, and token caching.
109+
110+
**Files to create:**
111+
- `src/auth/oauth/callback.rs``CallbackServer` with localhost HTTP listener, state validation, timeout
112+
- `src/auth/oauth/u2m.rs``AuthorizationCodeProvider` implementing `AuthProvider`
113+
114+
**Files to modify:**
115+
- `src/auth/oauth/mod.rs` — add `pub mod callback;`, `pub mod u2m;`, re-export `AuthorizationCodeProvider`
116+
- `src/auth/mod.rs` — re-export `AuthorizationCodeProvider`
117+
- `src/database.rs` — wire `AuthFlow::Browser` match arm in `new_connection()` to create `AuthorizationCodeProvider`
118+
119+
**Implementation details:**
120+
- On creation: try loading cached token via `TokenCache::load()`
121+
- If cached token has valid `refresh_token`: store in `TokenStore`, refresh on first `get_auth_header()` if stale/expired
122+
- If no cache: generate PKCE via `PkceCodeChallenge::new_random_sha256()`, build auth URL via `client.authorize_url()`, start `CallbackServer`, launch browser via `open::that()`, wait for callback, exchange code via `client.exchange_code().set_pkce_verifier()`
123+
- Save token to cache after every successful acquisition/refresh
124+
- Refresh uses `client.exchange_refresh_token()` through `DatabricksHttpClient::execute_without_auth()`
125+
- If refresh token is expired, fall back to full browser flow
126+
127+
**Tests:**
128+
- `callback.rs`: `test_callback_captures_code`, `test_callback_validates_state`, `test_callback_timeout`
129+
- `u2m.rs`: `test_u2m_refresh_token_flow`, `test_u2m_cache_hit`, `test_u2m_cache_miss_with_expired_refresh`
130+
- Wiremock integration: `test_u2m_refresh_token_full_flow`
131+
- Database validation: `test_new_connection_token_passthrough_missing_token`
132+
- E2E (ignored): `test_m2m_end_to_end`, `test_u2m_end_to_end`
133+
134+
**Definition of done:** U2M flow works end-to-end. Token cache persists across connections. Refresh path works via wiremock tests. Browser flow verified manually via `#[ignore]` E2E test.

0 commit comments

Comments
 (0)