From 4a1bf8db136b48960acfebc9e89cce53de5f68f3 Mon Sep 17 00:00:00 2001 From: wfurt Date: Sun, 29 Mar 2026 17:46:12 +0000 Subject: [PATCH 1/5] Improve NegotiateAuthentication test coverage --- .../NegotiateAuthenticationTest.cs | 313 ++++++++++++++++++ ...ystem.Net.Security.Enterprise.Tests.csproj | 1 + 2 files changed, 314 insertions(+) create mode 100644 src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs diff --git a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs new file mode 100644 index 00000000000000..b412b3a7831fe6 --- /dev/null +++ b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs @@ -0,0 +1,313 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.IO; +using System.Net.Test.Common; +using System.Security.Principal; +using System.Threading.Tasks; + +using Xunit; + +namespace System.Net.Security.Enterprise.Tests +{ + [ConditionalClass(typeof(EnterpriseTestConfiguration), nameof(EnterpriseTestConfiguration.Enabled))] + public class NegotiateAuthenticationTest + { + public static TheoryData AuthenticationSuccessCases => new TheoryData + { + { EnterpriseTestConfiguration.ValidNetworkCredentials, "HOST/localhost" }, + { EnterpriseTestConfiguration.ValidNetworkCredentials, "HOST/linuxclient.linux.contoso.com" }, + { EnterpriseTestConfiguration.ValidNetworkCredentials, "HTTP/apacheweb.linux.contoso.com" }, + }; + + public static TheoryData LoopbackAuthenticationSuccessCases => new TheoryData + { + { EnterpriseTestConfiguration.ValidNetworkCredentials, "HOST/localhost" }, + { EnterpriseTestConfiguration.ValidNetworkCredentials, "HOST/linuxclient.linux.contoso.com" }, + }; + + [Theory] + [MemberData(nameof(AuthenticationSuccessCases))] + public void ClientAuthentication_ValidCredentials_Succeeds(NetworkCredential credential, string targetName) + { + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = credential, + TargetName = targetName, + }); + + NegotiateAuthenticationStatusCode statusCode; + byte[]? token = client.GetOutgoingBlob(ReadOnlySpan.Empty, out statusCode); + + Assert.NotNull(token); + Assert.True(token.Length > 0); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + Assert.Equal("Negotiate", client.Package); + } + + [Theory] + [InlineData("HOST/localhost")] + [InlineData("HOST/linuxclient.linux.contoso.com")] + public void ClientAuthentication_DefaultCredentials_Succeeds(string targetName) + { + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = CredentialCache.DefaultNetworkCredentials, + TargetName = targetName, + }); + + NegotiateAuthenticationStatusCode statusCode; + byte[]? token = client.GetOutgoingBlob(ReadOnlySpan.Empty, out statusCode); + + if (token is null) + { + // No cached Kerberos TGT available (kinit not run); skip. + return; + } + + Assert.True(token.Length > 0); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + } + + [Theory] + [MemberData(nameof(LoopbackAuthenticationSuccessCases))] + public async Task ClientServerAuthentication_Succeeds(NetworkCredential credential, string targetName) + { + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = credential, + TargetName = targetName, + }); + + using var server = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions + { + Package = "Negotiate", + }); + + NegotiateAuthenticationStatusCode clientStatus, serverStatus; + + byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + + while (true) + { + byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); + if (serverStatus == NegotiateAuthenticationStatusCode.Completed) + { + if (serverToken is not null) + { + client.GetOutgoingBlob(serverToken, out clientStatus); + } + break; + } + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, serverStatus); + Assert.NotNull(serverToken); + + clientToken = client.GetOutgoingBlob(serverToken, out clientStatus); + if (clientStatus == NegotiateAuthenticationStatusCode.Completed) + { + break; + } + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + Assert.NotNull(clientToken); + } + + Assert.True(client.IsAuthenticated); + Assert.True(server.IsAuthenticated); + } + + [Theory] + [MemberData(nameof(LoopbackAuthenticationSuccessCases))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/12345")] + public async Task ClientServerAuthentication_WrapUnwrap_Succeeds(NetworkCredential credential, string targetName) + { + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = credential, + TargetName = targetName, + RequiredProtectionLevel = ProtectionLevel.EncryptAndSign, + }); + + using var server = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions + { + Package = "Negotiate", + RequiredProtectionLevel = ProtectionLevel.EncryptAndSign, + }); + + NegotiateAuthenticationStatusCode clientStatus, serverStatus; + byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); + + while (true) + { + byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); + if (serverStatus == NegotiateAuthenticationStatusCode.Completed) + { + break; + } + + clientToken = client.GetOutgoingBlob(serverToken, out clientStatus); + if (clientStatus == NegotiateAuthenticationStatusCode.Completed) + { + break; + } + } + + Assert.True(client.IsAuthenticated); + Assert.True(server.IsAuthenticated); + + byte[] message = "Hello from client"u8.ToArray(); + ArrayBufferWriter wrappedWriter = new ArrayBufferWriter(); + Assert.Equal(NegotiateAuthenticationStatusCode.Completed, client.Wrap(message, wrappedWriter, true, out bool isEncrypted)); + Assert.True(isEncrypted); + + ArrayBufferWriter unwrappedWriter = new ArrayBufferWriter(); + Assert.Equal(NegotiateAuthenticationStatusCode.Completed, server.Unwrap(wrappedWriter.WrittenSpan, unwrappedWriter, out bool wasEncrypted)); + Assert.True(wasEncrypted); + Assert.Equal(message, unwrappedWriter.WrittenSpan.ToArray()); + } + + [Fact] + public void ClientAuthentication_InvalidCredentials_Fails() + { + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = EnterpriseTestConfiguration.InvalidNetworkCredentials, + TargetName = "HOST/localhost", + }); + + using var server = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions + { + Package = "Negotiate", + }); + + NegotiateAuthenticationStatusCode clientStatus, serverStatus; + byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); + + if (clientStatus >= NegotiateAuthenticationStatusCode.GenericFailure) + { + return; + } + + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + Assert.NotNull(clientToken); + + bool authFailed = false; + while (true) + { + byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); + if (serverStatus >= NegotiateAuthenticationStatusCode.GenericFailure) + { + authFailed = true; + break; + } + if (serverStatus == NegotiateAuthenticationStatusCode.Completed) + { + break; + } + + clientToken = client.GetOutgoingBlob(serverToken!, out clientStatus); + if (clientStatus >= NegotiateAuthenticationStatusCode.GenericFailure) + { + authFailed = true; + break; + } + if (clientStatus == NegotiateAuthenticationStatusCode.Completed) + { + break; + } + } + + Assert.True(authFailed || !server.IsAuthenticated, + "Authentication should have failed with invalid credentials"); + } + + [Theory] + [InlineData(ProtectionLevel.Sign)] + [InlineData(ProtectionLevel.EncryptAndSign)] + public async Task ClientServerAuthentication_ProtectionLevel_Succeeds(ProtectionLevel protectionLevel) + { + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = EnterpriseTestConfiguration.ValidNetworkCredentials, + TargetName = "HOST/linuxclient.linux.contoso.com", + RequiredProtectionLevel = protectionLevel, + }); + + using var server = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions + { + Package = "Negotiate", + }); + + NegotiateAuthenticationStatusCode clientStatus, serverStatus; + byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); + + while (true) + { + byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); + if (serverStatus == NegotiateAuthenticationStatusCode.Completed) + { + if (serverToken is not null) + { + client.GetOutgoingBlob(serverToken, out clientStatus); + } + break; + } + + clientToken = client.GetOutgoingBlob(serverToken, out clientStatus); + if (clientStatus == NegotiateAuthenticationStatusCode.Completed) + { + break; + } + } + + Assert.True(client.IsAuthenticated); + Assert.True(client.IsSigned); + if (protectionLevel == ProtectionLevel.EncryptAndSign) + { + Assert.True(client.IsEncrypted); + } + } + + [Theory] + [InlineData("HOST/linuxclient.linux.contoso.com")] + [InlineData("HTTP/apacheweb.linux.contoso.com")] + public void ClientAuthentication_TargetName_ReturnsCorrectSPN(string targetName) + { + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = EnterpriseTestConfiguration.ValidNetworkCredentials, + TargetName = targetName, + }); + + Assert.Equal(targetName, client.TargetName); + } + + [Theory] + [InlineData("HOST/linuxclient.linux.contoso.com@LINUX.CONTOSO.COM")] + [InlineData("HTTP/apacheweb.linux.contoso.com@LINUX.CONTOSO.COM")] + public void ClientAuthentication_TargetNameWithRealm_Succeeds(string targetName) + { + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = EnterpriseTestConfiguration.ValidNetworkCredentials, + TargetName = targetName, + }); + + NegotiateAuthenticationStatusCode statusCode; + byte[]? token = client.GetOutgoingBlob(ReadOnlySpan.Empty, out statusCode); + + Assert.NotNull(token); + Assert.True(token.Length > 0); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + } + } +} diff --git a/src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj b/src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj index 3714c1bd6b9cab..dfd2bd8ed90117 100644 --- a/src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj @@ -5,6 +5,7 @@ + Date: Mon, 6 Apr 2026 06:01:24 +0000 Subject: [PATCH 2/5] feedback --- .../NegotiateAuthenticationTest.cs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs index b412b3a7831fe6..5594d54d4198a5 100644 --- a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.IO; using System.Net.Test.Common; using System.Security.Principal; -using System.Threading.Tasks; +using Microsoft.DotNet.XUnitExtensions; using Xunit; namespace System.Net.Security.Enterprise.Tests @@ -64,8 +63,7 @@ public void ClientAuthentication_DefaultCredentials_Succeeds(string targetName) if (token is null) { - // No cached Kerberos TGT available (kinit not run); skip. - return; + throw new SkipTestException("Kerberos TGT is not available (kinit not run)."); } Assert.True(token.Length > 0); @@ -74,7 +72,7 @@ public void ClientAuthentication_DefaultCredentials_Succeeds(string targetName) [Theory] [MemberData(nameof(LoopbackAuthenticationSuccessCases))] - public async Task ClientServerAuthentication_Succeeds(NetworkCredential credential, string targetName) + public void ClientServerAuthentication_Succeeds(NetworkCredential credential, string targetName) { using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions { @@ -93,7 +91,8 @@ public async Task ClientServerAuthentication_Succeeds(NetworkCredential credenti byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); - while (true) + const int MaxIterations = 20; + for (int i = 0; i < MaxIterations; i++) { byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); if (serverStatus == NegotiateAuthenticationStatusCode.Completed) @@ -123,7 +122,7 @@ public async Task ClientServerAuthentication_Succeeds(NetworkCredential credenti [Theory] [MemberData(nameof(LoopbackAuthenticationSuccessCases))] [ActiveIssue("https://github.com/dotnet/runtime/issues/12345")] - public async Task ClientServerAuthentication_WrapUnwrap_Succeeds(NetworkCredential credential, string targetName) + public void ClientServerAuthentication_WrapUnwrap_Succeeds(NetworkCredential credential, string targetName) { using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions { @@ -142,11 +141,16 @@ public async Task ClientServerAuthentication_WrapUnwrap_Succeeds(NetworkCredenti NegotiateAuthenticationStatusCode clientStatus, serverStatus; byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); - while (true) + const int MaxIterations = 20; + for (int i = 0; i < MaxIterations; i++) { byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); if (serverStatus == NegotiateAuthenticationStatusCode.Completed) { + if (serverToken is not null) + { + client.GetOutgoingBlob(serverToken, out clientStatus); + } break; } @@ -191,6 +195,7 @@ public void ClientAuthentication_InvalidCredentials_Fails() if (clientStatus >= NegotiateAuthenticationStatusCode.GenericFailure) { + // Authentication failed at the first step, which is acceptable. return; } @@ -198,7 +203,8 @@ public void ClientAuthentication_InvalidCredentials_Fails() Assert.NotNull(clientToken); bool authFailed = false; - while (true) + const int MaxIterations = 20; + for (int i = 0; i < MaxIterations; i++) { byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); if (serverStatus >= NegotiateAuthenticationStatusCode.GenericFailure) @@ -230,7 +236,7 @@ public void ClientAuthentication_InvalidCredentials_Fails() [Theory] [InlineData(ProtectionLevel.Sign)] [InlineData(ProtectionLevel.EncryptAndSign)] - public async Task ClientServerAuthentication_ProtectionLevel_Succeeds(ProtectionLevel protectionLevel) + public void ClientServerAuthentication_ProtectionLevel_Succeeds(ProtectionLevel protectionLevel) { using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions { @@ -248,7 +254,8 @@ public async Task ClientServerAuthentication_ProtectionLevel_Succeeds(Protection NegotiateAuthenticationStatusCode clientStatus, serverStatus; byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); - while (true) + const int MaxIterations = 20; + for (int i = 0; i < MaxIterations; i++) { byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); if (serverStatus == NegotiateAuthenticationStatusCode.Completed) From e6dd744a7caf7e28d0ffe2c9b6b899776c4a656e Mon Sep 17 00:00:00 2001 From: wfurt Date: Wed, 15 Apr 2026 20:57:34 +0000 Subject: [PATCH 3/5] fixes --- .../NegotiateAuthenticationTest.cs | 135 +++++++++++++++--- 1 file changed, 119 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs index 5594d54d4198a5..d6b44909c7e155 100644 --- a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs @@ -2,8 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics; +using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Test.Common; using System.Security.Principal; +using System.Threading.Tasks; using Microsoft.DotNet.XUnitExtensions; using Xunit; @@ -13,11 +17,35 @@ namespace System.Net.Security.Enterprise.Tests [ConditionalClass(typeof(EnterpriseTestConfiguration), nameof(EnterpriseTestConfiguration.Enabled))] public class NegotiateAuthenticationTest { + static NegotiateAuthenticationTest() + { + // Obtain a Kerberos TGT so that DefaultNetworkCredentials tests can work. + // Other tests pass explicit credentials but DefaultCredentials needs a cached ticket. + try + { + NetworkCredential creds = EnterpriseTestConfiguration.ValidNetworkCredentials; + using var process = new Process(); + process.StartInfo.FileName = "kinit"; + process.StartInfo.Arguments = $"{creds.UserName}@{EnterpriseTestConfiguration.Realm}"; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.UseShellExecute = false; + process.Start(); + process.StandardInput.WriteLine(creds.Password); + process.StandardInput.Close(); + process.WaitForExit(10_000); + } + catch + { + // kinit may not be available; the test will skip gracefully. + } + } + public static TheoryData AuthenticationSuccessCases => new TheoryData { { EnterpriseTestConfiguration.ValidNetworkCredentials, "HOST/localhost" }, { EnterpriseTestConfiguration.ValidNetworkCredentials, "HOST/linuxclient.linux.contoso.com" }, { EnterpriseTestConfiguration.ValidNetworkCredentials, "HTTP/apacheweb.linux.contoso.com" }, + { EnterpriseTestConfiguration.ValidNetworkCredentials, "HTTP/apacheweb.linux.contoso.com@LINUX.CONTOSO.COM" }, }; public static TheoryData LoopbackAuthenticationSuccessCases => new TheoryData @@ -46,7 +74,7 @@ public void ClientAuthentication_ValidCredentials_Succeeds(NetworkCredential cre Assert.Equal("Negotiate", client.Package); } - [Theory] + [ConditionalTheory] [InlineData("HOST/localhost")] [InlineData("HOST/linuxclient.linux.contoso.com")] public void ClientAuthentication_DefaultCredentials_Succeeds(string targetName) @@ -58,16 +86,47 @@ public void ClientAuthentication_DefaultCredentials_Succeeds(string targetName) TargetName = targetName, }); - NegotiateAuthenticationStatusCode statusCode; - byte[]? token = client.GetOutgoingBlob(ReadOnlySpan.Empty, out statusCode); + using var server = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions + { + Package = "Negotiate", + }); + + NegotiateAuthenticationStatusCode clientStatus, serverStatus; + byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); - if (token is null) + if (clientToken is null) { throw new SkipTestException("Kerberos TGT is not available (kinit not run)."); } - Assert.True(token.Length > 0); - Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + + const int MaxIterations = 20; + for (int i = 0; i < MaxIterations; i++) + { + byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); + if (serverStatus == NegotiateAuthenticationStatusCode.Completed) + { + if (serverToken is not null) + { + client.GetOutgoingBlob(serverToken, out clientStatus); + } + break; + } + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, serverStatus); + Assert.NotNull(serverToken); + + clientToken = client.GetOutgoingBlob(serverToken, out clientStatus); + if (clientStatus == NegotiateAuthenticationStatusCode.Completed) + { + break; + } + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + Assert.NotNull(clientToken); + } + + Assert.True(client.IsAuthenticated); + Assert.True(server.IsAuthenticated); } [Theory] @@ -121,7 +180,6 @@ public void ClientServerAuthentication_Succeeds(NetworkCredential credential, st [Theory] [MemberData(nameof(LoopbackAuthenticationSuccessCases))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/12345")] public void ClientServerAuthentication_WrapUnwrap_Succeeds(NetworkCredential credential, string targetName) { using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions @@ -297,11 +355,10 @@ public void ClientAuthentication_TargetName_ReturnsCorrectSPN(string targetName) Assert.Equal(targetName, client.TargetName); } - [Theory] - [InlineData("HOST/linuxclient.linux.contoso.com@LINUX.CONTOSO.COM")] - [InlineData("HTTP/apacheweb.linux.contoso.com@LINUX.CONTOSO.COM")] - public void ClientAuthentication_TargetNameWithRealm_Succeeds(string targetName) + [Fact] + public async Task ClientServerAuthentication_AgainstWebServer_Succeeds() { + string targetName = "HTTP/apacheweb.linux.contoso.com"; using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions { Package = "Negotiate", @@ -309,12 +366,58 @@ public void ClientAuthentication_TargetNameWithRealm_Succeeds(string targetName) TargetName = targetName, }); - NegotiateAuthenticationStatusCode statusCode; - byte[]? token = client.GetOutgoingBlob(ReadOnlySpan.Empty, out statusCode); + byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out NegotiateAuthenticationStatusCode clientStatus); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + Assert.NotNull(clientToken); - Assert.NotNull(token); - Assert.True(token.Length > 0); - Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, statusCode); + using var httpClient = new HttpClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, EnterpriseTestConfiguration.NegotiateAuthWebServer); + request.Headers.Authorization = new AuthenticationHeaderValue("Negotiate", Convert.ToBase64String(clientToken)); + + using HttpResponseMessage response = await httpClient.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + string? serverAuthHeader = response.Headers.WwwAuthenticate.ToString(); + if (!string.IsNullOrEmpty(serverAuthHeader) && serverAuthHeader.StartsWith("Negotiate ", StringComparison.OrdinalIgnoreCase)) + { + byte[] serverToken = Convert.FromBase64String(serverAuthHeader.Substring("Negotiate ".Length)); + client.GetOutgoingBlob(serverToken, out clientStatus); + } + + Assert.True(client.IsAuthenticated); + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/12345")] + public async Task ClientServerAuthentication_AgainstWebServer_WithRealmHint_Succeeds() + { + string targetName = "HTTP/apacheweb.linux.contoso.com@LINUX.CONTOSO.COM"; + using var client = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions + { + Package = "Negotiate", + Credential = EnterpriseTestConfiguration.ValidNetworkCredentials, + TargetName = targetName, + }); + + byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out NegotiateAuthenticationStatusCode clientStatus); + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + Assert.NotNull(clientToken); + + using var httpClient = new HttpClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, EnterpriseTestConfiguration.NegotiateAuthWebServer); + request.Headers.Authorization = new AuthenticationHeaderValue("Negotiate", Convert.ToBase64String(clientToken)); + + using HttpResponseMessage response = await httpClient.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + string? serverAuthHeader = response.Headers.WwwAuthenticate.ToString(); + if (!string.IsNullOrEmpty(serverAuthHeader) && serverAuthHeader.StartsWith("Negotiate ", StringComparison.OrdinalIgnoreCase)) + { + byte[] serverToken = Convert.FromBase64String(serverAuthHeader.Substring("Negotiate ".Length)); + client.GetOutgoingBlob(serverToken, out clientStatus); + } + + Assert.True(client.IsAuthenticated); } } } From 39009b049983941b017e7961908911b72e55d856 Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 16 Apr 2026 00:27:36 +0000 Subject: [PATCH 4/5] fix import --- .../EnterpriseTests/NegotiateAuthenticationTest.cs | 1 - .../libs/System.Net.Security.Native/pal_gssapi.c | 13 +++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs index d6b44909c7e155..bf7f87dad8a932 100644 --- a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs @@ -388,7 +388,6 @@ public async Task ClientServerAuthentication_AgainstWebServer_Succeeds() } [Fact] - [ActiveIssue("https://github.com/dotnet/runtime/issues/12345")] public async Task ClientServerAuthentication_AgainstWebServer_WithRealmHint_Succeeds() { string targetName = "HTTP/apacheweb.linux.contoso.com@LINUX.CONTOSO.COM"; diff --git a/src/native/libs/System.Net.Security.Native/pal_gssapi.c b/src/native/libs/System.Net.Security.Native/pal_gssapi.c index d793217a4a671d..db646050971865 100644 --- a/src/native/libs/System.Net.Security.Native/pal_gssapi.c +++ b/src/native/libs/System.Net.Security.Native/pal_gssapi.c @@ -242,7 +242,20 @@ uint32_t NetSecurityNative_ImportPrincipalName(uint32_t* minorStatus, // Principal name will usually be in the form SERVICE/HOST. But SPNEGO protocol prefers // GSS_C_NT_HOSTBASED_SERVICE format. That format uses '@' separator instead of '/' between // service name and host name. So convert input string into that format. + // + // If the input contains both '/' and '@' (e.g. SERVICE/HOST@REALM), it is a fully + // qualified Kerberos principal name with an explicit realm. Import it directly as + // GSS_KRB5_NT_PRINCIPAL_NAME so the realm hint is respected. char* ptrSlash = (char*)memchr(inputName, '/', inputNameLen); + char* ptrAt = (char*)memchr(inputName, '@', inputNameLen); + if (ptrSlash != NULL && ptrAt != NULL) + { + static gss_OID_desc gss_krb5_nt_principal_name_desc = + {10, "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02\x01"}; + GssBuffer inputNameBuffer = {.length = inputNameLen, .value = inputName}; + return gss_import_name(minorStatus, &inputNameBuffer, &gss_krb5_nt_principal_name_desc, outputName); + } + char* inputNameCopy = NULL; if (ptrSlash != NULL) { From 886392f5a7f228945842cab5f5f4f587a3c11eb0 Mon Sep 17 00:00:00 2001 From: wfurt Date: Tue, 21 Apr 2026 22:15:59 +0000 Subject: [PATCH 5/5] feedback --- .../NegotiateAuthenticationTest.cs | 177 +++++++----------- 1 file changed, 66 insertions(+), 111 deletions(-) diff --git a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs index bf7f87dad8a932..216c79ea474e2e 100644 --- a/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs +++ b/src/libraries/System.Net.Security/tests/EnterpriseTests/NegotiateAuthenticationTest.cs @@ -6,7 +6,6 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.Test.Common; -using System.Security.Principal; using System.Threading.Tasks; using Microsoft.DotNet.XUnitExtensions; @@ -91,7 +90,7 @@ public void ClientAuthentication_DefaultCredentials_Succeeds(string targetName) Package = "Negotiate", }); - NegotiateAuthenticationStatusCode clientStatus, serverStatus; + NegotiateAuthenticationStatusCode clientStatus; byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); if (clientToken is null) @@ -101,32 +100,7 @@ public void ClientAuthentication_DefaultCredentials_Succeeds(string targetName) Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); - const int MaxIterations = 20; - for (int i = 0; i < MaxIterations; i++) - { - byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); - if (serverStatus == NegotiateAuthenticationStatusCode.Completed) - { - if (serverToken is not null) - { - client.GetOutgoingBlob(serverToken, out clientStatus); - } - break; - } - Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, serverStatus); - Assert.NotNull(serverToken); - - clientToken = client.GetOutgoingBlob(serverToken, out clientStatus); - if (clientStatus == NegotiateAuthenticationStatusCode.Completed) - { - break; - } - Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); - Assert.NotNull(clientToken); - } - - Assert.True(client.IsAuthenticated); - Assert.True(server.IsAuthenticated); + AssertMutualAuthenticationCompleted(client, server, clientToken); } [Theory] @@ -145,37 +119,7 @@ public void ClientServerAuthentication_Succeeds(NetworkCredential credential, st Package = "Negotiate", }); - NegotiateAuthenticationStatusCode clientStatus, serverStatus; - - byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); - Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); - - const int MaxIterations = 20; - for (int i = 0; i < MaxIterations; i++) - { - byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); - if (serverStatus == NegotiateAuthenticationStatusCode.Completed) - { - if (serverToken is not null) - { - client.GetOutgoingBlob(serverToken, out clientStatus); - } - break; - } - Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, serverStatus); - Assert.NotNull(serverToken); - - clientToken = client.GetOutgoingBlob(serverToken, out clientStatus); - if (clientStatus == NegotiateAuthenticationStatusCode.Completed) - { - break; - } - Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); - Assert.NotNull(clientToken); - } - - Assert.True(client.IsAuthenticated); - Assert.True(server.IsAuthenticated); + AssertMutualAuthenticationCompleted(client, server); } [Theory] @@ -196,31 +140,7 @@ public void ClientServerAuthentication_WrapUnwrap_Succeeds(NetworkCredential cre RequiredProtectionLevel = ProtectionLevel.EncryptAndSign, }); - NegotiateAuthenticationStatusCode clientStatus, serverStatus; - byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); - - const int MaxIterations = 20; - for (int i = 0; i < MaxIterations; i++) - { - byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); - if (serverStatus == NegotiateAuthenticationStatusCode.Completed) - { - if (serverToken is not null) - { - client.GetOutgoingBlob(serverToken, out clientStatus); - } - break; - } - - clientToken = client.GetOutgoingBlob(serverToken, out clientStatus); - if (clientStatus == NegotiateAuthenticationStatusCode.Completed) - { - break; - } - } - - Assert.True(client.IsAuthenticated); - Assert.True(server.IsAuthenticated); + AssertMutualAuthenticationCompleted(client, server, assertContinueNeeded: false); byte[] message = "Hello from client"u8.ToArray(); ArrayBufferWriter wrappedWriter = new ArrayBufferWriter(); @@ -309,30 +229,8 @@ public void ClientServerAuthentication_ProtectionLevel_Succeeds(ProtectionLevel Package = "Negotiate", }); - NegotiateAuthenticationStatusCode clientStatus, serverStatus; - byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); - - const int MaxIterations = 20; - for (int i = 0; i < MaxIterations; i++) - { - byte[]? serverToken = server.GetOutgoingBlob(clientToken, out serverStatus); - if (serverStatus == NegotiateAuthenticationStatusCode.Completed) - { - if (serverToken is not null) - { - client.GetOutgoingBlob(serverToken, out clientStatus); - } - break; - } - - clientToken = client.GetOutgoingBlob(serverToken, out clientStatus); - if (clientStatus == NegotiateAuthenticationStatusCode.Completed) - { - break; - } - } + AssertMutualAuthenticationCompleted(client, server, assertContinueNeeded: false); - Assert.True(client.IsAuthenticated); Assert.True(client.IsSigned); if (protectionLevel == ProtectionLevel.EncryptAndSign) { @@ -368,11 +266,11 @@ public async Task ClientServerAuthentication_AgainstWebServer_Succeeds() byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out NegotiateAuthenticationStatusCode clientStatus); Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); - Assert.NotNull(clientToken); + byte[] nonNullClientToken = Assert.IsType(clientToken); using var httpClient = new HttpClient(); using var request = new HttpRequestMessage(HttpMethod.Get, EnterpriseTestConfiguration.NegotiateAuthWebServer); - request.Headers.Authorization = new AuthenticationHeaderValue("Negotiate", Convert.ToBase64String(clientToken)); + request.Headers.Authorization = new AuthenticationHeaderValue("Negotiate", Convert.ToBase64String(nonNullClientToken)); using HttpResponseMessage response = await httpClient.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -400,11 +298,11 @@ public async Task ClientServerAuthentication_AgainstWebServer_WithRealmHint_Succ byte[]? clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out NegotiateAuthenticationStatusCode clientStatus); Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); - Assert.NotNull(clientToken); + byte[] nonNullClientToken = Assert.IsType(clientToken); using var httpClient = new HttpClient(); using var request = new HttpRequestMessage(HttpMethod.Get, EnterpriseTestConfiguration.NegotiateAuthWebServer); - request.Headers.Authorization = new AuthenticationHeaderValue("Negotiate", Convert.ToBase64String(clientToken)); + request.Headers.Authorization = new AuthenticationHeaderValue("Negotiate", Convert.ToBase64String(nonNullClientToken)); using HttpResponseMessage response = await httpClient.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -418,5 +316,62 @@ public async Task ClientServerAuthentication_AgainstWebServer_WithRealmHint_Succ Assert.True(client.IsAuthenticated); } + + private static void AssertMutualAuthenticationCompleted( + NegotiateAuthentication client, + NegotiateAuthentication server, + byte[]? initialClientToken = null, + bool assertContinueNeeded = true) + { + NegotiateAuthenticationStatusCode clientStatus; + byte[]? clientToken = initialClientToken; + + if (clientToken is null) + { + clientToken = client.GetOutgoingBlob(ReadOnlySpan.Empty, out clientStatus); + if (assertContinueNeeded) + { + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + Assert.NotNull(clientToken); + } + } + + NegotiateAuthenticationStatusCode serverStatus; + const int MaxIterations = 20; + + for (int i = 0; i < MaxIterations; i++) + { + byte[]? serverToken = server.GetOutgoingBlob(clientToken!, out serverStatus); + if (serverStatus == NegotiateAuthenticationStatusCode.Completed) + { + if (serverToken is not null) + { + client.GetOutgoingBlob(serverToken, out clientStatus); + } + break; + } + + if (assertContinueNeeded) + { + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, serverStatus); + Assert.NotNull(serverToken); + } + + clientToken = client.GetOutgoingBlob(serverToken!, out clientStatus); + if (clientStatus == NegotiateAuthenticationStatusCode.Completed) + { + break; + } + + if (assertContinueNeeded) + { + Assert.Equal(NegotiateAuthenticationStatusCode.ContinueNeeded, clientStatus); + Assert.NotNull(clientToken); + } + } + + Assert.True(client.IsAuthenticated); + Assert.True(server.IsAuthenticated); + } } }