diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs index 14c2609cf3..9e83d58793 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs @@ -168,20 +168,8 @@ internal override void CreatePhysicalSNIHandle( string hostNameInCertificate, string serverCertificateFilename) { - if (isIntegratedSecurity) - { - // 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("<{0}.{1}|SEC> Server SPN `{2}` from the connection string is used.", nameof(TdsParserStateObjectNative), nameof(CreatePhysicalSNIHandle), serverSPN); - } - else - { - // This will signal to the interop layer that we need to retrieve the SPN - serverSPN = string.Empty; - } - } + // Normalize SPN based on authentication mode + serverSPN = NormalizeServerSpn(serverSPN, isIntegratedSecurity); ConsumerInfo myInfo = CreateConsumerInfo(async); SQLDNSInfo cachedDNSInfo; @@ -189,7 +177,44 @@ internal override void CreatePhysicalSNIHandle( _sessionHandle = new SNIHandle(myInfo, serverName, ref serverSPN, timeout.MillisecondsRemainingInt, out instanceName, flushCache, !async, fParallel, ipPreference, cachedDNSInfo, hostNameInCertificate); - resolvedSpn = new(serverSPN.TrimEnd()); + + // Only produce resolvedSpn when we actually have one. + if (!string.IsNullOrWhiteSpace(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) + { + 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 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 new file mode 100644 index 0000000000..5315a99d96 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/TdsParserStateObjectNativeTests.cs @@ -0,0 +1,35 @@ +// 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. + +#if NETCOREAPP && WINDOWS + +#nullable enable + +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(" ", 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 + whitespace -> null (no generation) + [InlineData("MSSQLSvc/host", false, "MSSQLSvc/host")] // SQL Auth + provided -> use it + public void NormalizeServerSpn_ReturnsExpectedValue( + string? inputSpn, + bool isIntegratedSecurity, + string? expectedSpn) + { + string? result = TdsParserStateObjectNative.NormalizeServerSpn(inputSpn, isIntegratedSecurity); + Assert.Equal(expectedSpn, result); + } + } +} + +#endif