Add ActiveDirectoryInteractive auth to mssql-odbc via mssql-auth (T3) - #145
Conversation
Implement the ActiveDirectory Interactive method behind the existing EntraIdTokenFactory seam by hand-rolling the OAuth2 authorization-code flow with PKCE (the Azure SDK ships no interactive credential). Bind a loopback listener, open the system browser at the /authorize endpoint, receive the redirect, and exchange the code at /token as a public client. The redirect handler requires a matching state before honoring any code or error, reads the request line incrementally under a per-connection timeout, and ignores unrelated local callbacks. Empty access tokens are rejected so they are not cached. The login-connect deadline is disabled for interactive so the browser/MFA flow has time to complete. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds browser-based Active Directory Interactive authentication to the ODBC driver using OAuth2 authorization code flow with PKCE.
Changes:
- Implements browser launch, loopback redirect handling, token exchange, and caching.
- Registers interactive authentication with the TDS FedAuth pipeline.
- Adds dependencies and unit tests for PKCE, redirects, URLs, and token parsing.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
mssql-odbc/src/auth/mod.rs |
Registers the interactive authentication module. |
mssql-odbc/src/auth/interactive.rs |
Implements and tests the interactive OAuth2 flow. |
mssql-odbc/src/auth/entra.rs |
Configures the interactive token factory and shared helpers. |
mssql-odbc/src/api/driver_connect.rs |
Updates authentication support and unsupported-method testing. |
mssql-odbc/Cargo.toml |
Adds networking, PKCE, randomness, and JSON dependencies. |
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changes
Summary
mssql-odbc/src/api/driver_connect.rsmssql-odbc/src/api/exports.rsmssql-odbc/src/api/get_connect_attr.rsmssql-odbc/src/auth/interactive.rsmssql-odbc/src/auth/msqa.rsmssql-odbc/src/handles/dbc.rsmssql-tds/src/connection_provider/tds_connection_provider.rs🔗 Quick Links |
- connect_timeout=0 gave every TCP connect a 0ms budget (timeout(0ms, ...) fires immediately), failing the connection before the browser opened. Raise it to a bounded 330s (CONNECT_TIMEOUT_SECS) so the login stays bounded and the TCP connect keeps a real budget, mirroring SqlClient's enlarged Connect Timeout for interactive auth. - Stop caching the access token in a OnceCell: like the service-principal path, acquire a fresh token each login so session recovery cannot reuse an expired token. Caching/refresh is tracked in AB#46409. - Drop the misleading "open this URL manually" hint from the browser-launch error; the loopback listener is gone once the error returns. - Reap the macOS open / Linux xdg-open child on a detached thread so it does not linger as a zombie. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Split the mssql-tds single connect_timeout into a network-connect cap (connect_timeout) and an overall login deadline (login_timeout), matching msodbcsql's separate connection vs. login timeouts. login_timeout is an Option<u32> that falls back to connect_timeout when unset, so every existing caller keeps identical behavior. Wire the ODBC SQL_ATTR_LOGIN_TIMEOUT attribute to login_timeout and have the interactive (T3) browser flow raise login_timeout instead of connect_timeout. An unreachable server now still fails fast on the per-TCP-connect cap while the browser/MFA round-trip has time to complete. An app-set SQL_ATTR_LOGIN_TIMEOUT takes precedence over the interactive default. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Pushed mssql-tds core (additive, back-compatible):
mssql-odbc:
Net effect: the black-hole-server concern from the earlier review is resolved properly rather than papered over. Validation: |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
mssql-odbc/src/auth/interactive.rs:315
READ_TIMEOUTis restarted for every read, so a local client that sends one byte every few seconds can monopolize this serial handler until the full redirect timeout and prevent the real browser callback from being processed. Apply one timeout to the entire request-line read (or track an absolute deadline), rather than timing each individual read.
let n = match tokio::time::timeout(READ_TIMEOUT, stream.read(&mut chunk)).await {
Ok(Ok(n)) if n > 0 => n,
_ => break, // EOF, read error, or timeout
};
mssql-odbc/src/auth/interactive.rs:435
- The token response is aggregated without any size limit. Because this endpoint is derived from server-provided FEDAUTHINFO, a malicious or malfunctioning HTTPS authority can stream an arbitrarily large body and exhaust the host application's memory before the outer login timeout fires. Read chunks with a small explicit cap appropriate for an OAuth JSON response and reject responses that exceed it.
let status = response.status();
let bytes = response
.bytes()
.await
.map_err(|e| Error::ConnectionError(format!("failed to read token response: {e}")))?;
mssql-odbc/src/auth/interactive.rs:422
- The client uses reqwest's default redirect policy, which follows cross-origin and HTTPS-to-HTTP redirects. A 307/308 response from the token endpoint would therefore resend the authorization code and PKCE verifier to the redirect target, bypassing the module's HTTPS-only endpoint check. OAuth token endpoints do not need redirects here; disable them (or validate every redirect target before following it).
let client = reqwest::Client::builder()
.build()
.map_err(|e| Error::ConnectionError(format!("failed to build HTTP client: {e}")))?;
…deadline ordering - Implement SQLGetConnectAttrW for SQL_ATTR_LOGIN_TIMEOUT so a set/get round-trip returns the stored value (driver default 15 when unset); add round-trip, default, and null-pointer tests. - Redact the interactive authorize URL debug log: drop the query string (CSRF state, PKCE challenge, login_hint) and log only scheme/host/path. - Compute the login deadline before the Windows shared-memory shortcut so it bounds the SM/SSRP/LocalDB phases too; the SM attempt now uses the remaining login budget instead of connect_timeout, so interactive sign-in against a local named instance isn't cancelled after the default 15s. Back-compatible for callers that only set connect_timeout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
mssql-tds/src/connection_provider/tds_connection_provider.rs:150
- This deadline still does not bound every phase as claimed. Both SSRP calls at lines 223/227 await their independent
ssrp_timeout_ms, and LocalDB resolution at line 266 performs a blockingLocalDBStartInstancecall without consultingdeadline; for example,login_timeout = 1withssrp_timeout_ms = 10_000can run for roughly ten seconds. Apply the remaining login budget to these resolution phases too, and add a regression test with distinct login and SSRP timeouts.
let login_timeout = context.login_timeout.unwrap_or(context.connect_timeout);
let deadline = match login_timeout {
1.. => Some(Instant::now() + Duration::from_secs(login_timeout.into())),
_ => None,
};
Getter (SQLGetConnectAttrW): refactor get_connect_attr.rs to the crate's canonical panic-boundary -> unsafe shim -> safe-core layering (mirroring get_data.rs); write the value via write_if_some; report a null value pointer with post_diag(ERR_INVALID_NULL_POINTER); log buffer_length and string_length_ptr in the entry trace. Interactive auth (interactive.rs): - Bound the browser-redirect wait by the effective login_timeout instead of a fixed 300s, so an app SQL_ATTR_LOGIN_TIMEOUT above/below the default (or 0 = infinite) is honored. The factory now carries the effective login timeout and derives the cap via redirect_wait_cap; REDIRECT_TIMEOUT stays as the fallback. - Cap the /token response body at 1 MiB (read_body_capped) so a hostile FEDAUTHINFO-selected authority cannot stream an unbounded body into memory. - Reap the browser-launcher child via thread::Builder::spawn and handle the error instead of thread::spawn, which panics on OS thread-creation failure (a shared library must not unwind across the FFI boundary). Tests: +3 redirect_wait_cap unit tests. fmt + clippy (-D warnings) clean; 352 mssql-odbc lib tests and 8 connection_provider tests pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
mssql-tds/src/connection_provider/tds_connection_provider.rs:150
- This only calculates a deadline; it does not bound the SSRP and LocalDB awaits below (lines 223/227 and 266). For example, an app-set 1-second login timeout can still wait for a longer SSRP timeout before the retry loop ever checks
deadline, so the new overall-login contract is not enforced. Wrap those resolution phases in the remaining budget, or apply the timeout around the whole action-chain future.
let login_timeout = context.login_timeout.unwrap_or(context.connect_timeout);
let deadline = match login_timeout {
1.. => Some(Instant::now() + Duration::from_secs(login_timeout.into())),
_ => None,
};
Address Copilot review on PR #145: - Map interactive auth denials (user cancel / access_denied, and permanent token-endpoint failures) to Error::Security(SecurityError::AuthenticationDenied), which is non-transient, so the connect-retry loop no longer relaunches the browser on a terminal auth outcome. - Bound the loopback request-line read by a single overall deadline instead of a per-read timeout, so a slow local client cannot hold the sequential callback handler open and starve the real browser redirect. Add tests: bounded request-line read behavior (mssql-odbc) and a regression guard that AuthenticationDenied is not a transient connect error (mssql-tds). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
mssql-tds/src/connection_provider/tds_connection_provider.rs:142
- This deadline does not actually bound every phase named here. The SSRP calls at lines 223/227 use only
ssrp_timeout_ms, and LocalDB resolution at line 266 is also awaited without the remaining login budget. For example, a caller can setlogin_timeout = 1andssrp_timeout_ms = 30, yet named-instance resolution can run for 30 seconds before the deadline is checked. Wrap these resolution phases in the remaining deadline (and move blocking LocalDB work off the runtime thread) sologin_timeoutis truly an overall cap.
// Compute the overall login deadline up front so it bounds every
// phase of login — the shared-memory shortcut below, SSRP/LocalDB
// resolution, and the connect/retry loop — not just the final
// transport attempts. `login_timeout` falls back to `connect_timeout`
// for callers that only set the historical single knob; `0` means
The deadline bounds the shared-memory shortcut and the connect/retry loop, but not name resolution: SSRP uses its own ssrp_timeout_ms and LocalDB resolution is a local pipe lookup. Describe that accurately instead of claiming it bounds every phase of login. Addresses a low-confidence Copilot note on PR #145. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
mssql-odbc/src/auth/interactive.rs:565
- A successful HTTP status with malformed JSON or an empty token is converted to
ConnectionError, whichis_transient_connect_errorretries. That retry restarts the entire connection and opens a second browser even though the authority already returned a definitive invalid response. Classify response-validation failures as a non-transient protocol error.
let token = serde_json::from_str::<TokenResponse>(body)
.map(|token| token.access_token)
.map_err(|e| Error::ConnectionError(format!("failed to parse token response: {e}")))?;
if token.is_empty() {
return Err(Error::ConnectionError(
"token response contained an empty access token".into(),
));
Address Copilot round-4 review on PR #145: - tds_connection_provider: the shared-memory shortcut runs the full handshake, so it can now return a non-transient AuthenticationDenied (interactive sign-in cancelled). Only fall through to SSRP/TCP on transient failures; return non-transient errors immediately so a cancelled login does not relaunch the browser. - get_connect_attr: unsupported attributes now return SQL_ERROR/HYC00 instead of SQL_SUCCESS without writing, matching the set-side. - interactive: a 200 OK with a malformed body or empty access_token is a protocol failure, not a transient blip; map both to ProtocolError so they stay out of the connect retry loop. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The previous id a94f9c62-97fe-4d19-b06d-472bed8d2bcb does not exist in Entra (AADSTS700016), so interactive sign-in could never have succeeded. Present msodbcsql's ODBC client id instead so both drivers are a single identity in Entra. That app is registered for Windows broker redirects only, so the loopback flow stays blocked until http://localhost is added (AB#46683). Document the pending registration and the late AADSTS50011 failure, and make the redirect timeout name the browser-side cause instead of expiring silently. Also document the per-platform mechanism: loopback is the only option on Linux/macOS and matches SqlClient, while Windows moves to mssql-auth (OneAuth/WAM) under AB#46684 with this flow as the fallback. Add mock token-endpoint tests covering the request form, OAuth error mapping, malformed and empty responses, and an unreachable endpoint, plus coverage for the redirect-wait timeout arm. AB#46067 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replaces the loopback/browser interactive flow with the same OneAuth (mssql-auth.dll) path the C++ msodbcsql driver uses, so both drivers behave identically for Authentication=ActiveDirectoryInteractive. This matters because mssql-odbc presents msodbcsql's own client id: a keyword that behaved differently between the two drivers would be a behavioural fork, not parity. msodbcsql compiles its interactive support on Windows only. SNI_FedAuth is absent from the Unix SRCLIST, its header is wrapped in `#if !defined(MPLAT_BUILD)`, and the DLL is loaded with LoadLibraryExA and has no dlopen counterpart. On Linux and macOS the MSQA branch and its else arm both vanish with the surrounding guard, so Parse.cpp falls through to AzureADAuth with AKVCFG_AUTHMODE_INTEGRATED. This change reproduces that fall-through rather than rejecting the keyword, so callers see one behaviour across drivers. Parity parameters taken from Parse.cpp:3601-3620: client id 2c1229aa-16c5-4ff5-b46b-4f7fe2a2a9c8, redirect uri https://sqlaad/, WAM off by default, and the "Authenticate to database on %s" window title. OneAuth status codes are unpacked from the high byte of the returned error, and only NetworkTemporarilyUnavailable, ServerTemporarilyUnavailable and TransientError are treated as retryable, mirroring IsTransientError. A cancelled sign-in therefore fails once instead of reopening the window. Also adds SQL_ATTR_LOGIN_TIMEOUT with a real SQLGetConnectAttrW, and closes an open sign-in window when the caller's login deadline expires. Verified against a live Azure SQL server: sign-in succeeds, a second connection reuses the cached context without prompting, and cancelling fails once with SQLSTATE 08001. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolves mssql-odbc/Cargo.toml, where main added [dev-dependencies] and this branch added the Windows-only [target.'cfg(windows)'.dependencies] block for the mssql-auth sign-in host. Both sections are kept. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Heads-up before you re-review: this PR has been redesigned, so a fresh read is needed rather than a diff against your last review. The loopback + PKCE + system-browser flow is gone. Interactive sign-in now delegates to Why: we present msodbcsql's own client id, so the two drivers have to behave identically to callers and to the service. Hand-rolling our own OAuth flow behind that client id was the wrong call — same identity, different behaviour. What that means for your earlier comments: the threads about PKCE, Platform behaviour: interactive is Windows-only, matching msodbcsql ( Also still in scope: Verified live against Azure SQL, not just unit tests: sign-in completes, a second connection reuses the cached context without prompting (0.1s vs 19.4s), and cancelling fails exactly once without reopening the window. One open question worth your input: Details and parity citations are in the updated PR description. |
Two conflicts from #149: - exports.rs: main still had the SQLGetConnectAttrW stub returning SQL_ERROR; this branch implements it, so the implementation is kept. - set_connect_attr.rs: main added SQL_ATTR_ANSI_APP to the accept-and-ignore arm. That arm is kept with SQL_ATTR_ANSI_APP, minus SQL_ATTR_LOGIN_TIMEOUT, which this branch handles in its own arm above; leaving it in both would be an unreachable pattern. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
mssql-odbc/src/auth/msqa.rs:350
- This also recovers a poisoned mutex and proceeds with a context whose acquisition state may be inconsistent. Return an error on poison rather than using the inner value, as required for mutexes in the ODBC driver.
let _serialized = context
.acquire_lock
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
mssql-odbc/src/auth/msqa.rs:202
- This non-test
expectviolates the ODBC shared-library's panic-free requirement. Propagate the construction failure through the cachedResultrather than introducing a panic path, even though the current constant is valid.
let name = CString::new(MSQA_LIBRARY).expect("library name has no interior NUL");
mssql-odbc/src/auth/msqa.rs:612
- A zero id does not necessarily mean there is nothing left to cancel: the
spawn_blockingtask may still be queued or waiting onacquire_lockand has not calledsetyet. If the login deadline expires in that window, this returns, then the detached task can later open a sign-in window with no caller remaining to close it. Record cancellation independently of the thread id and have registration/UI startup observe that flag before creating the window.
pub(super) fn cancel_ui(ui_thread_id: &UiThreadId) {
let thread_id = ui_thread_id.get();
if thread_id == 0 {
return;
mssql-odbc/src/auth/msqa.rs:282
- Recovering a poisoned mutex with
into_inner()is unsafe for this in-process driver because the cache may have been left inconsistent by the panic. Follow the crate's poison policy and return an authentication error instead of continuing with potentially corrupted state.
This issue also appears on line 347 of the same file.
let mut cache = cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
mssql-odbc/src/auth/interactive.rs:150
mem::forgetleaks the guard'sArcon every completed token acquisition. Disarm the guard with explicit state (for example, anOption/armed flag checked byDrop) so successful calls release the allocation without posting cancellation.
fn disarm(self) {
std::mem::forget(self);
mssql-odbc/src/auth/msqa.rs:278
- The PR description says context-cache keying and error-description reads are unit-tested, but this module's tests never call
get_or_create_contextordescribe_failure; they only test status constants, the atomic id, NUL rejection, and bit extraction. Add injectable/fake MSQA API tests that verify reuse/separation by(login_hint, sts_url)and both passes of error-description retrieval, or update the stated coverage.
let key = (login_hint.to_string(), sts_url.to_string());
msodbcsql caps the login timeout at MAX_QUERY_TIMEOUT (0xfffe) and posts 01S02 "Login timeout changed" when it has to clamp (sqlcmisc.cpp:1735). This driver accepted any value, so it diverged on the boundary and, worse, narrowed with a raw `as u32`: a request for 2^32 seconds wrapped to 0, which the connect path reads as "wait indefinitely". Clamping at pointer width before narrowing makes that wrap unreachable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
GetMessageW returns >0 for a message, 0 for WM_QUIT and -1 on error, but BOOL::as_bool() is a plain != 0 test, so the error return read as "message available". The pump would then dispatch an uninitialized MSG and, if the condition persisted, spin forever - and because run_interactive_ui blocks on the thread's join, the connection would never return and the STA thread would leak. msodbcsql tests `> 0` for this reason (SNI_FedAuth.cpp:474); handle the three cases separately to match. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Review thread triageI went back through all 23 open threads and validated each one against the code as it stands now rather than as it stood when the comment was written. Two pushes since: 1f0a7b6 and 0f08e9c. One was a live bug and is fixed (0f08e9c) - One more parity gap I found while re-reading, fixed (1f0a7b6) - 14 were obsolete, all describing the browser/loopback/PKCE flow that the MSQA pivot deleted. Verified by symbol, not assumption - 6 were fixed in earlier rounds and simply never marked resolved - the 1 was valid but inert - Those 22 are now resolved. I left Shiwani Gupta (@shiwanigupta0809)'s client-id question open since it is hers to close - short version: the id was wrong when you asked and is now taken verbatim from |
David Engel (David-Engel)
left a comment
There was a problem hiding this comment.
Review summary
Adds Authentication=ActiveDirectoryInteractive to mssql-odbc by delegating to mssql-auth.dll (OneAuth/MSQA) on Windows, splits connect_timeout into a per-TCP-connect cap and a new overall login_timeout, and implements a real SQLGetConnectAttrW for SQL_ATTR_LOGIN_TIMEOUT.
The redesign away from a hand-rolled PKCE flow is the right call, and the FFI layer is careful: RAII request guard, dedicated STA thread, the GetMessageW > 0 fix, embedded-NUL rejection, and traceable msodbcsql references throughout. cargo test -p mssql-odbc --lib passes locally on Linux (440 tests).
Findings are inline. Two I'd like resolved before merge:
WM_QUITcan be posted to a recycled thread id incancel_ui— the load-then-post is unsynchronized, and in an ODBC driver hosted inside an arbitrary application this can tear down the host app's message loop.- The off-Windows diagnostic names a method the caller never asked for — a Linux user who writes
Authentication=ActiveDirectoryInteractivegets "Authentication method ActiveDirectoryIntegrated is not yet supported", which is undiagnosable.
The rest are suggestions and nits.
Tests
Coverage of the pure logic is good: clamping, 0 to infinite, app-set precedence, transient classification, cache-key round-trip, NUL rejection. The gap is that roughly 690 lines of msqa.rs — the pump, the cancel path, the token and error reads — are exercised only by the manual live run, and both blocking findings live in exactly that untested region. Given the 85% bar, consider extracting the cancel/thread-id lifecycle behind a small trait so the race is testable without the DLL.
|
Re-queued validation (build 164519). The previous run failed only on |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Fix the WM_QUIT cancellation race: UiThreadId becomes a mutex so the pump cannot clear its id, return, and let the OS recycle the id onto another thread between the load and the post. Route OneAuth's completion callback through the same guarded path, since OneAuth gives no guarantee that a callback cannot arrive after a cancelled pump has already exited. Name the requested authentication method off Windows. The keyword still resolves to ActiveDirectoryIntegrated for msodbcsql parity, but the diagnostic no longer reports a method the connection string never used. Answer SQLGetConnectAttrW for every attribute SQLSetConnectAttrW accepts, storing the values so a set/get round-trip agrees, and reject a post-connect SQL_ATTR_PACKET_SIZE with HY011 as msodbcsql does. Retry a failed mssql-auth.dll load rather than caching the failure for the life of the process, and return a distinct error when the sign-in thread cannot initialize COM instead of feeding an HRESULT to MSQAGetErrorDescription. Share the connect timeout default as DEFAULT_CONNECT_TIMEOUT_SECS, document the empty-UID account reuse and the access token length convention, and cover both arms of the shared-memory fall-through policy. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The per-account sign-in lock was a std Mutex taken inside acquire_token, which runs under spawn_blocking. A second connection to the same account parked a blocking-pool thread on it with no deadline of its own: when its login timeout expired the future was dropped, but the blocking task stayed parked until the sign-in ahead of it finished. Split context resolution out of acquire_token so the lock can be taken on the async side as a tokio Mutex via lock_owned(). A caller that hits its deadline while waiting is now dropped from the waiter queue instead of occupying a thread. The guard moves into the blocking task so the lock is still held for the real duration of the acquisition, including a cancelled one's teardown. Related work item: AB#46980 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
David Engel (David-Engel)
left a comment
There was a problem hiding this comment.
Re-review
All eleven findings from the previous pass are addressed, and several went further than what I asked for:
- The thread-id fix also re-routes
ui_completethrough the guarded path, so a late OneAuth callback can't post at a recycled thread either — not just the deadline canceller. - The
HY011question was answered better than I posed it: the guard belongs onSQL_ATTR_PACKET_SIZE(fixed by LOGIN7) and not onSQL_ATTR_LOGIN_TIMEOUT, with the msodbcsql citation showing it stores that one unconditionally on a reusable handle. - The async
sign_in_lockcorrectly keeps the guard inside the blocking task, so a dropped future can't release it while the window is still tearing down. The current-thread-runtime tests are a neat way to prove the waiter yields rather than parks.
cargo test -p mssql-odbc --lib passes (446) and cargo clippy -p mssql-odbc -p mssql-tds --all-targets is clean on Linux.
Two nits inline, neither merge-blocking.
Where I'd still spend manual attention
Everything remaining is Windows-only FFI that CI cannot reach:
ui_complete's pointer lifetime. The callback now dereferencesdataas&UiThreadId. The ordering in the code is right —MSQADeleteRequestruns insideacquire_tokenbefore the closure'sArcdrops — but the safety argument rests on OneAuth never invoking the callback after the request is deleted, which is an assumption about a closed-source DLL. The fix traded a narrow stray-WM_QUITwindow for a use-after-free invariant, so it's worth confirming the MSQA contract actually guarantees it.- Live re-run of the deadline-expiry-with-window-open path, since
post_quitnow holds a lock acrossPostThreadMessageWwhile the pump may be insideDispatchMessageW.PostThreadMessageWis documented as non-blocking so this should be fine, but it is exactly the path the change touches and no test can reach it. - A DM smoke test for the new
SQL_ATTR_PACKET_SIZEHY011, which is new behaviour for every connection rather than just interactive ones.
|
Make sure to merge this branch with the latest origin/main to get the benefits of coverage reporting. |
disarm() used mem::forget, which suppressed Drop for the Arc as well as the canceller, so every successful interactive sign-in leaked a UiThreadId allocation. Hold it in an Option and clear that instead. The callback pointer's validity never depended on this leak: the Arc moved into the blocking closure outlives MSQADeleteRequest on every path, which is what guarantees ui_complete's data pointer. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
SQL_ATTR_CONNECTION_TIMEOUT reused WARN_LOGIN_TIMEOUT_CHANGED, so clamping it reported `Login timeout changed` for an attribute the application never set. msodbcsql has the same defect -- one arm covers both timeouts and posts IDS_01_S02_05 for either (sqlcmisc.cpp:1739, local.rc:45) -- so this is a deliberate divergence. The SQLSTATE applications branch on is unchanged; only the human-readable text is corrected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Saurabh Singh (@saurabh500) Done — merged Head is now 40c0f70. |
Description
Implements T3:
Authentication=ActiveDirectoryInteractivefor mssql-odbc, matching the classic C++msodbcsqldriver.Why the design changed
mssql-odbc presents msodbcsql's own client id. The two drivers therefore have to be indistinguishable to callers and to the service — a keyword that behaved differently between them would be a behavioural fork, not parity. Delegating to the same auth library yields identical prompts, identical caching semantics, and identical error text, and avoids this repo owning a hand-rolled OAuth implementation.
Platform behaviour
mssql-auth.dllActiveDirectoryIntegrated(msodbcsql's own fall-through)msodbcsql's interactive support is Windows-only. From the C++ source:
SNI_FedAuth.hpp:16wraps the entire header in#if !defined(MPLAT_BUILD) //No MSQA for non-windows clientssni/src/MakefileomitsSNI_FedAuthfromSRCLIST— it is never compiled on UnixSNI_FedAuth.cpp:249loads the DLL viaLoadLibraryExA; the repo contains nodlopencounterpartOn Unix the MSQA branch and its
elsearm both sit inside#if !defined(XPLAT_ODBC_TODO)(Parse.cpp:3597-3660), so the guard removes both and control falls through toAzureADAuthwithAKVCFG_AUTHMODE_INTEGRATED. Nothing rejects the keyword earlier (sqlcconn.cpp:4066-4078accepts it on every platform).An explicit "interactive is not supported on this platform" error was prototyped and deliberately withdrawn: because we present msodbcsql's client id, refusing a keyword msodbcsql accepts would be the one place the drivers visibly disagree. Improving on the fall-through is tracked separately rather than shipped as a silent divergence.
How it works
InteractiveTokenFactoryimplements the existing mssql-tdsEntraIdTokenFactoryseam, so it plugs into the FedAuth handshake exactly like the T2 service-principal factory.auth/msqa.rsholds themssql-auth.dllbindings and mirrorsSNI_FedAuth.cpp:username + "\0" + stsUrl(SNI_FedAuth.cpp:127), created withForcePrompt=TRUE, andForcePromptis cleared after the first success — so a second connection reuses the context and does not re-prompt.MSQAGetRequestStatusdecides whether interaction is needed; when it is, the window is hosted on a dedicated STA thread (CoInitializeEx(COINIT_APARTMENTTHREADED)+MSQAUICreateHostWindow+ message pump), and the completion callback ends the pump viaPostThreadMessage(WM_QUIT).MSQAReleaseAuthenticationContextis intentionally not called for interactive (SNI_FedAuth.cpp:777-782) — the context is the token cache.Statusinto the high byte of the returned error. OnlyNetworkTemporarilyUnavailable,ServerTemporarilyUnavailableandTransientErrorare retryable, mirroringIsTransientError(:301-306).UserCanceledis not transient, so a cancelled sign-in fails once instead of reopening the window.Parity parameters, all from
Parse.cpp:3601-3620:2c1229aa-16c5-4ff5-b46b-4f7fe2a2a9c8(msodbcsql's ODBC client id)https://sqlaad/ADALuseWAMdefaults to 0,sqlcconn.cpp:3417)"Authenticate to database on %s"(IDS_AD_AUTH_DB)Login timeout
Also lands
SQL_ATTR_LOGIN_TIMEOUTand a realSQLGetConnectAttrW. mssql-tds previously had a singleconnect_timeoutbounding both the overall login deadline and each TCP connect, so allowing minutes for an MFA prompt also let an unreachable host hang that long. These are now split, matching msodbcsql:connect_timeout(default 15s) — per-TCP-connect cap; unreachable servers still fail fast.login_timeout: Option<u32>(new) — overall login deadline.Nonefalls back toconnect_timeout, so existing callers are unaffected;Some(0)waits indefinitely.Interactive installs a 330s default only when the app has not set its own
SQL_ATTR_LOGIN_TIMEOUT. If the deadline expires while a window is open,CancelUiOnDroppostsWM_QUITso the window is torn down rather than orphaned.Tests
cargo test -p mssql-odbc --lib→ 419 pass. Unit tests cover context-cache keying, transient/permanent status classification, error-description reads,configure_authdispatch on both platforms, and the login-timeout attribute (including0→ infinite and preserving an app-set value).Because Linux/macOS cannot be cross-compiled here (
openssl-sys), the non-Windows path was validated by inverting everycfg(windows)/cfg_attrgate and building: clippy clean under-D warnings, 333 tests pass. That caught a real break — an unused-variable error that would only have appeared in Linux CI.Live verification
The sign-in window and token read cannot run in CI, so this was verified manually against a live Azure SQL server and Entra tenant:
SQL_SUCCESS_WITH_INFO;SUSER_NAME()returns the Entra accountSQLSTATE 08001, one failureSQL_ATTR_LOGIN_TIMEOUTround-tripsKnown limitations
mssql-auth.dllis not redistributed by this driver. Interactive works where msodbcsql 18 has already installed it intoSystem32. Packaging is unresolved and needs a decision before GA.FEDAUTHINFO(carrying the STS URL) before a token can be acquired, so the connection idles for the whole sign-in; a multi-minute first-time MFA can exceed the Azure SQL gateway's idle timeout. The token is acquired successfully — the failure surfaces from the TDS login parser. msodbcsql prompts from the same point in the login exchange and shares this exposure, so it is treated as parity, not a defect here.Related Issues
AB#46067
Checklist
cargo bfmtpassescargo bclippypassescargo btestpasses —cargo test -p mssql-odbc --lib(419 pass) locally;nextest/llvm-covunavailable on this machine, so full coverage runs in CI