From e653eefb9935abd91c4f1bffea57ae2d90b545c1 Mon Sep 17 00:00:00 2001 From: Apoorv Deshmukh Date: Tue, 3 Feb 2026 19:22:50 +0530 Subject: [PATCH 1/4] Generate SPN only for integrated security auth modes --- .../TdsParserStateObjectNative.windows.cs | 51 ++++++++++++++----- .../TdsParserStateObjectNativeTests.cs | 28 ++++++++++ 2 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.windows.cs index 1269cf8827..9778aa2f5f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.windows.cs @@ -155,21 +155,16 @@ internal override void CreatePhysicalSNIHandle( string hostNameInCertificate, string serverCertificateFilename) { - if (isIntegratedSecurity) + + if (isIntegratedSecurity && !string.IsNullOrEmpty(serverSPN)) { - // now allocate proper length of buffer - if (!string.IsNullOrEmpty(serverSPN)) - { - // Native SNI requires the Unicode encoding and any other encoding like UTF8 breaks the code. - SqlClientEventSource.Log.TryTraceEvent(" Server SPN `{0}` from the connection string is used.", serverSPN); - } - else - { - // Empty signifies to interop layer that SPN needs to be generated - serverSPN = string.Empty; - } + // Native SNI requires the Unicode encoding and any other encoding like UTF8 breaks the code. + SqlClientEventSource.Log.TryTraceEvent(" Server SPN `{0}` from the connection string is used.", serverSPN); } + // Normalize SPN based on authentication mode + serverSPN = NormalizeServerSpn(serverSPN, isIntegratedSecurity); + ConsumerInfo myInfo = CreateConsumerInfo(async); // serverName : serverInfo.ExtendedServerName @@ -183,7 +178,37 @@ internal override void CreatePhysicalSNIHandle( transparentNetworkResolutionState, totalTimeout, #endif iPAddressPreference, cachedDNSInfo, hostNameInCertificate); - resolvedSpn = new(serverSPN.TrimEnd()); + + // Only produce resolvedSpn when integrated security is in play and we actually have one. + if (isIntegratedSecurity && !string.IsNullOrEmpty(serverSPN)) + { + resolvedSpn = new(serverSPN.TrimEnd()); + } + else + { + resolvedSpn = default; + } + } + + /// + /// Normalizes the serverSPN based on authentication mode. + /// + /// The server SPN value from the connection string. + /// Indicates whether integrated security (SSPI) is being used. + /// + /// For integrated security: returns if provided, otherwise to trigger SPN generation. + /// For SQL auth: returns if is empty (no generation), otherwise returns the provided value. + /// + internal static string NormalizeServerSpn(string serverSPN, bool isIntegratedSecurity) + { + if (isIntegratedSecurity) + { + // Empty signifies to interop layer that SPN needs to be generated + return string.IsNullOrEmpty(serverSPN) ? string.Empty : serverSPN; + } + + // For SQL auth (and other non-SSPI modes), null means "No SPN generation". + return serverSPN == string.Empty ? null : serverSPN; } protected override uint SniPacketGetData(PacketHandle packet, byte[] _inBuff, ref uint dataSize) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs new file mode 100644 index 0000000000..defe4838db --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests +{ + public class TdsParserStateObjectNativeTests + { + [Theory] + [InlineData(null, true, "")] // Integrated + null -> empty (generate SPN) + [InlineData("", true, "")] // Integrated + empty -> empty (generate SPN) + [InlineData("MSSQLSvc/host", true, "MSSQLSvc/host")] // Integrated + provided -> use it + [InlineData(null, false, null)] // SQL Auth + null -> null (no generation) + [InlineData("", false, null)] // SQL Auth + empty -> null (no generation) + [InlineData("MSSQLSvc/host", false, "MSSQLSvc/host")] // SQL Auth + provided -> use it + [PlatformSpecific(TestPlatforms.Windows)] + public void NormalizeServerSpn_ReturnsExpectedValue( + string? inputSpn, + bool isIntegratedSecurity, + string? expectedSpn) + { + string? result = TdsParserStateObjectNative.NormalizeServerSpn(inputSpn, isIntegratedSecurity); + Assert.Equal(expectedSpn, result); + } + } +} From 8908dde393223d17fb591c759c729fe18c9354eb Mon Sep 17 00:00:00 2001 From: Apoorv Deshmukh Date: Tue, 3 Feb 2026 21:38:15 +0530 Subject: [PATCH 2/4] Add unit test only for windows --- .../Data/SqlClient/TdsParserStateObjectNativeTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs index defe4838db..8b40bc5a0a 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#if NETFRAMEWORK || WINDOWS + using Xunit; namespace Microsoft.Data.SqlClient.UnitTests @@ -26,3 +28,5 @@ public void NormalizeServerSpn_ReturnsExpectedValue( } } } + +#endif From 1e6f1ca7ef92f1ee74b46ee84fad6054bd33f53d Mon Sep 17 00:00:00 2001 From: Apoorv Deshmukh Date: Wed, 4 Feb 2026 16:35:41 +0530 Subject: [PATCH 3/4] Address review comment and enhance SPN normalization logic --- .../TdsParserStateObjectNative.windows.cs | 24 +++++++++---------- .../TdsParserStateObjectNativeTests.cs | 2 ++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.windows.cs index 9778aa2f5f..d04e3798a7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.windows.cs @@ -155,13 +155,6 @@ internal override void CreatePhysicalSNIHandle( string hostNameInCertificate, string serverCertificateFilename) { - - if (isIntegratedSecurity && !string.IsNullOrEmpty(serverSPN)) - { - // Native SNI requires the Unicode encoding and any other encoding like UTF8 breaks the code. - SqlClientEventSource.Log.TryTraceEvent(" Server SPN `{0}` from the connection string is used.", serverSPN); - } - // Normalize SPN based on authentication mode serverSPN = NormalizeServerSpn(serverSPN, isIntegratedSecurity); @@ -179,8 +172,8 @@ internal override void CreatePhysicalSNIHandle( #endif iPAddressPreference, cachedDNSInfo, hostNameInCertificate); - // Only produce resolvedSpn when integrated security is in play and we actually have one. - if (isIntegratedSecurity && !string.IsNullOrEmpty(serverSPN)) + // Only produce resolvedSpn when we actually have one. + if (!string.IsNullOrWhiteSpace(serverSPN)) { resolvedSpn = new(serverSPN.TrimEnd()); } @@ -203,12 +196,19 @@ internal static string NormalizeServerSpn(string serverSPN, bool isIntegratedSec { if (isIntegratedSecurity) { - // Empty signifies to interop layer that SPN needs to be generated - return string.IsNullOrEmpty(serverSPN) ? string.Empty : serverSPN; + if (string.IsNullOrWhiteSpace(serverSPN)) + { + // Empty signifies to interop layer that SPN needs to be generated + return string.Empty; + } + + // Native SNI requires the Unicode encoding and any other encoding like UTF8 breaks the code. + SqlClientEventSource.Log.TryTraceEvent(" Server SPN `{0}` from the connection string is used.", serverSPN); + return serverSPN; } // For SQL auth (and other non-SSPI modes), null means "No SPN generation". - return serverSPN == string.Empty ? null : serverSPN; + return string.IsNullOrWhiteSpace(serverSPN) ? null : serverSPN; } protected override uint SniPacketGetData(PacketHandle packet, byte[] _inBuff, ref uint dataSize) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs index 8b40bc5a0a..5192bad9ee 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs @@ -13,9 +13,11 @@ public class TdsParserStateObjectNativeTests [Theory] [InlineData(null, true, "")] // Integrated + null -> empty (generate SPN) [InlineData("", true, "")] // Integrated + empty -> empty (generate SPN) + [InlineData(" ", true, "")] // Integrated + empty -> empty (generate SPN) [InlineData("MSSQLSvc/host", true, "MSSQLSvc/host")] // Integrated + provided -> use it [InlineData(null, false, null)] // SQL Auth + null -> null (no generation) [InlineData("", false, null)] // SQL Auth + empty -> null (no generation) + [InlineData(" ", false, null)] // SQL Auth + empty -> null (no generation) [InlineData("MSSQLSvc/host", false, "MSSQLSvc/host")] // SQL Auth + provided -> use it [PlatformSpecific(TestPlatforms.Windows)] public void NormalizeServerSpn_ReturnsExpectedValue( From fe855a7faea6a79b85faf20bb8619902bb3f8369 Mon Sep 17 00:00:00 2001 From: Apoorv Deshmukh Date: Wed, 4 Feb 2026 16:45:33 +0530 Subject: [PATCH 4/4] Address review comments --- .../Data/SqlClient/TdsParserStateObjectNativeTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs index 5192bad9ee..3b33634ac7 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs @@ -13,11 +13,11 @@ public class TdsParserStateObjectNativeTests [Theory] [InlineData(null, true, "")] // Integrated + null -> empty (generate SPN) [InlineData("", true, "")] // Integrated + empty -> empty (generate SPN) - [InlineData(" ", true, "")] // Integrated + empty -> empty (generate SPN) + [InlineData(" ", true, "")] // Integrated + whitespace -> empty (generate SPN) [InlineData("MSSQLSvc/host", true, "MSSQLSvc/host")] // Integrated + provided -> use it [InlineData(null, false, null)] // SQL Auth + null -> null (no generation) [InlineData("", false, null)] // SQL Auth + empty -> null (no generation) - [InlineData(" ", false, null)] // SQL Auth + empty -> null (no generation) + [InlineData(" ", false, null)] // SQL Auth + whitespace -> null (no generation) [InlineData("MSSQLSvc/host", false, "MSSQLSvc/host")] // SQL Auth + provided -> use it [PlatformSpecific(TestPlatforms.Windows)] public void NormalizeServerSpn_ReturnsExpectedValue(