Fix AccessTokenCallback TNIR behavior and make SspiContextProvider mutually exclusive with token auth - #4520
Fix AccessTokenCallback TNIR behavior and make SspiContextProvider mutually exclusive with token auth#4520cheenamalhotra wants to merge 6 commits into
Conversation
On .NET Framework the driver disables Transparent Network IP Resolution by default whenever federated authentication is in use, unless the caller explicitly specified the TransparentNetworkIPResolution keyword. However, ShouldDisableTnir only tested _accessTokenInBytes (SqlConnection.AccessToken) and ignored _accessTokenCallback (SqlConnection.AccessTokenCallback), so the two token-supplying APIs behaved differently. Raised in review discussion on #4493. Changes: * Add SqlConnectionInternal.IsAccessTokenProvided, a single source of truth for "the caller supplied a token, either literally or via a callback", and use it in all three places that previously inlined the field checks (ShouldDisableTnir plus two spots in TdsParser.ConsumePreLoginHandshake). The duplicated, hand-written expression is what allowed the two paths to drift apart. * Fix the AccessToken, AccessTokenCallback and SspiContextProvider setters, which each rebuilt the ConnectionPoolKey with the sibling authentication values hard-coded to null. Setting SspiContextProvider silently dropped a previously assigned access token or callback from the pool key, so it never reached the internal connection even though the public property still reported it as set. These now preserve sibling state, matching the ConnectionString setter. (AccessToken and AccessTokenCallback are already mutually exclusive, so that pairing was benign; SspiContextProvider is not.) * Expose ShouldDisableTnir as internal static so it can be unit tested, and add coverage for the TNIR decision matrix and for pool-key preservation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5368f578-219b-40a6-92a9-4742b56edbe6
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR tightens behavior around federated authentication state by ensuring access-token state is consistently represented in connection pooling keys and by unifying “caller-supplied token” detection for TNIR and pre-login logic.
Changes:
- Preserve
AccessToken/AccessTokenCallback/SspiContextProviderinConnectionPoolKeyupdates to avoid silently dropping authentication state. - Introduce
SqlConnectionInternal.IsAccessTokenProvidedand apply it in pre-login and certificate validation checks. - Add NETFRAMEWORK TNIR tests and simulated server tests covering token/callback state and mutual exclusivity.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs | Adds regression tests for pool-key preservation and token/callback mutual exclusivity. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionOptionsTest.cs | Adds NETFRAMEWORK theory coverage for TNIR disablement with caller-supplied tokens. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs | Switches token detection to unified IsAccessTokenProvided. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs | Updates pool-key rebuilds to preserve other auth-related properties. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs | Adds IsAccessTokenProvided and refactors NETFRAMEWORK TNIR logic to accept it as an input. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
* Fully qualify the SqlConnection crefs on ShouldDisableTnir so they match the form already used on IsAccessTokenProvided. * Rename TestShouldDisableTnirWithAccessToken to TestShouldDisableTnirWithCallerSuppliedToken, since the parameter is isAccessTokenProvided and the case covers both AccessToken and AccessTokenCallback. * Assign a real non-null SspiContextProvider in the pool-key test instead of null, via a minimal TestSspiContextProvider stub, so the test matches its name and exercises the setter the way callers actually do. * Add SspiContextProviderIsPreservedInPoolKeyWhenAccessTokenStateIsSet, the reciprocal case: assigning a token must not drop a configured SspiContextProvider from the pool key. Both pool-key tests were verified to fail when the SqlConnection.cs fix is reverted. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5368f578-219b-40a6-92a9-4742b56edbe6
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4520 +/- ##
==========================================
- Coverage 64.73% 62.66% -2.08%
==========================================
Files 288 283 -5
Lines 44088 67057 +22969
==========================================
+ Hits 28542 42020 +13478
- Misses 15546 25037 +9491
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
SSPI is an alternative to token-based authentication, not a complement to it. Previously the SspiContextProvider setter silently coexisted with AccessToken/AccessTokenCallback, leaving the connection in an ambiguous authentication state. Both token setters now throw when a context provider is already set, and the SspiContextProvider setter throws when either token property is already set. The setters continue to pass the sibling values through when building the ConnectionPoolKey: with the new validation those values are guaranteed null when a non-null value is assigned, so it only matters on the clearing path, where the remaining authentication state must survive. Also scope the IsAccessTokenProvided remark to the call sites where the token and callback paths are genuinely equivalent. Login feature-extension negotiation must keep testing the fields individually because they select different federated authentication library types (MSAL vs SecurityToken), and the previous wording invited a cleanup that would break fedauth login. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5368f578-219b-40a6-92a9-4742b56edbe6
mdaigle
left a comment
There was a problem hiding this comment.
Requesting changes for the clone bypass, the Credential setter pool-key divergence, and missing TNIR coverage through the connection open paths.
| if (value != null) | ||
| { | ||
| // SSPI is an alternative to token-based authentication, so the two are mutually exclusive. | ||
| CheckAndThrowOnInvalidCombinationOfConnectionOptionAndSspiContextProvider(); |
There was a problem hiding this comment.
Could we copy _sspiContextProvider in the copy constructor and add a clone regression test? ICloneable.Clone() copies _accessToken and _accessTokenCallback, but not _sspiContextProvider, while CopyFrom retains the source PoolGroup. I verified that the clone reports SspiContextProvider == null while its pool key still carries the provider, and assigning AccessToken to the clone succeeds instead of throwing. This bypasses the new mutual-exclusivity validation and can authenticate with state that the public properties do not report.
| // token-based authentication, so a context provider cannot be combined with AccessToken or | ||
| // AccessTokenCallback. If there is any conflict, it throws InvalidOperationException. | ||
| // This is to be used by the setter of the SspiContextProvider property. | ||
| private void CheckAndThrowOnInvalidCombinationOfConnectionOptionAndSspiContextProvider() |
There was a problem hiding this comment.
Could we make the Credential setter preserve or validate SspiContextProvider? It still rebuilds the pool key with sspiContextProvider: null. I verified that after setting a provider and then Credential, the public property still reports the provider while the pool key has dropped it. This leaves the same property/pool-key divergence this change fixes in the other setters.
| [InlineData("my.test.server", true, true, false)] | ||
| [InlineData("test.database.windows.net", true, true, false)] | ||
| [InlineData("test.database.windows.net", false, true, false)] | ||
| public void TestShouldDisableTnirWithCallerSuppliedToken( |
There was a problem hiding this comment.
Could we add regression coverage through both Open and OpenAsync with AccessTokenCallback? This theory supplies isAccessTokenProvided as a literal, so it does not exercise IsAccessTokenProvided or LoginNoFailover. Reverting the property to _accessTokenInBytes != null leaves these tests green. Please also cover an explicit TransparentNetworkIPResolution=false; the current explicit-keyword rows only test true.
Follow-up to the review discussion on #4493, where it was noted that the TNIR behavior documented there does not actually apply when
SqlConnection.AccessTokenCallbackis used:The bug
On .NET Framework the driver disables Transparent Network IP Resolution by default whenever federated authentication is in use, unless the caller explicitly specified the
TransparentNetworkIPResolutionkeyword.SqlConnectionInternal.ShouldDisableTnironly tested_accessTokenInBytes(SqlConnection.AccessToken) and ignored_accessTokenCallback(SqlConnection.AccessTokenCallback), so the two token-supplying APIs behaved differently for no good reason.Investigating the fix surfaced a second, coupled defect in how the authentication setters build the connection pool key. Both are fixed here.
Changes
1. Single source of truth for "a token was supplied."
Added
SqlConnectionInternal.IsAccessTokenProvidedand used it in all three places that previously inlined the field checks —ShouldDisableTnirplus two spots inTdsParser.ConsumePreLoginHandshake. The duplicated hand-written expression is exactly what let these paths drift apart, and the existing@TODOinOnFedAuthInfopredicted this ("we're gonna forget one in one spot and cause a big ol bug someday").The property's doc comment is deliberately scoped: it is the right check for prelogin FEDAUTHREQUIRED, server certificate validation and TNIR, but not for login feature-extension negotiation, which must keep testing the fields individually because
_accessTokenCallbackselectsFedAuthLibrary.MSALwhile_accessTokenInBytesselectsFedAuthLibrary.SecurityToken. Calling that out prevents a future "cleanup" from collapsing those two sites and breaking fedauth login.2.
SspiContextProvideris now mutually exclusive with token authentication.The
AccessToken,AccessTokenCallbackandSspiContextProvidersetters each rebuilt theConnectionPoolKeywith the sibling authentication values hard-coded tonull. SettingSspiContextProvidersilently dropped a previously assigned access token or callback from the pool key, so it never reached the internal connection even though the public property still reported it as set — which would also have defeated the TNIR fix above.SSPI is an alternative to token-based authentication rather than a complement to it, so the correct fix is not to preserve both but to reject the combination outright:
CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken/...AccessTokenCallbacknow throw if a context provider is already set.CheckAndThrowOnInvalidCombinationOfConnectionOptionAndSspiContextProvider, called from theSspiContextProvidersetter, throws if either token property is already set.AccessTokenandAccessTokenCallbackwere already mutually exclusive (validated inCheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessToken*), so that pairing was always benign;SspiContextProviderwas the live defect.The setters still pass the sibling values through when constructing the
ConnectionPoolKey. With the new validation in place this is a no-op whenever a non-null value is assigned — the siblings are guaranteed null — so it matters only on the clearing path. Validation only runs whenvalue != null(matching the existing convention in the token setters), soconn.AccessToken = "t"; conn.SspiContextProvider = null;is a clear, not a combination, and does not throw. Passing literal nulls there would wipe the token from the pool key whileconn.AccessTokenstill reported it as set, which is the same class of bug this PR is fixing.3. Tests.
ShouldDisableTniris nowinternal staticso the decision matrix can be unit tested directly (constructing aSqlConnectionInternalin a unit test is impractical). Added:SqlConnectionOptionsTest.TestShouldDisableTnirWithCallerSuppliedToken(netfx only) — token/no-token x Azure/non-Azure endpoint x explicit/absent TNIR keyword.ConnectionTests.SspiContextProviderAndAccessTokenStateAreMutuallyExclusive— all four assignment orderings across both token properties.ConnectionTests.ClearingOneAuthPropertyPreservesTheOthersInPoolKey— pins the clearing-path behavior described above; verified to fail without theSqlConnection.cschange.ConnectionTests.AccessTokenAndAccessTokenCallbackAreMutuallyExclusive— pins the pre-existing invariant that makes the token pairing safe.Compatibility
The TNIR change is limited to .NET Framework, and only to connections using
AccessTokenCallback, which now get the same TNIR default asAccessToken. Users who explicitly setTransparentNetworkIPResolutionin the connection string are unaffected — the explicit keyword still takes precedence, so the escape hatch documented in #4493 continues to work.The SSPI change is a deliberate behavior change on all targets: assigning
SspiContextProvideralongsideAccessToken/AccessTokenCallbacknow throwsInvalidOperationExceptioninstead of silently producing an ambiguous authentication state. Any code relying on the old behavior was already broken — the token was being dropped from the pool key — so this converts silent misbehavior into a clear, actionable error. Clearing a property (assigningnull) is unaffected and never throws.Checklist
Suggested release note
Fixed
SqlConnection.AccessTokenCallbacknot disabling Transparent Network IP Resolution by default on .NET Framework, making it consistent withSqlConnection.AccessToken.SqlConnection.SspiContextProvideris now correctly treated as mutually exclusive withAccessTokenandAccessTokenCallbackand throws when combined, instead of silently discarding the token from the connection pool key.Notes for reviewers
SspiTestssuite; the net462 target builds clean but its tests cannot be executed on the macOS dev machine used here, so CI covers that leg.dotnet/SqlClientso the full CI/ADO pipeline suite runs.