Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Commit 1bb52e6

Browse files
davidshwtgodbe
authored andcommitted
Fix SocketsHttpHandler proxy auth for 'Negotiate' scheme (#39933) (#39981)
Issue #39887 reported that proxy authentication with 'Negotiate' scheme broke between .NET Core 3.0 Preview 6 and Preview 7. The base64 blob was no longer using SPNEGO protocol but instead was always using NTLM. While 'Negotiate' scheme can use either SPNEGO or NTLM, it should always use SPNEGO if possible. And many enterprises have a setting which requires it and rejects NTLM protocol. This issue was caused by PR #38465 which fixed some other SPN issues with Kerberos authentication. That PR regressed the SPN calculation for the proxy authentication by using the wrong host name in the SPN. A mismatch of the SPN will cause NTLM to be used instead of SPNEGO. The fix is to check if proxy authentication is being used instead of server authentication. If so, it ignores any 'Host' header and always will use the uri, which in this case is the uri of the proxy server. This was tested manually. It is impossible right now to test Kerberos and proxy scenarios in CI because they require machine configuration to register SPNs in a Windows Active Directory environment. This PR will be ported for release/3.0 for ASK mode consideration since it affects a mainline enterprise scenario. Fixes #39887
1 parent 7c56101 commit 1bb52e6

File tree

2 files changed

+52
-1
lines changed

2 files changed

+52
-1
lines changed

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,10 @@ private static async Task<HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMe
8383

8484
// Calculate SPN (Service Principal Name) using the host name of the request.
8585
// Use the request's 'Host' header if available. Otherwise, use the request uri.
86+
// Ignore the 'Host' header if this is proxy authentication since we need to use
87+
// the host name of the proxy itself for SPN calculation.
8688
string hostName;
87-
if (request.HasHeaders && request.Headers.Host != null)
89+
if (!isProxyAuth && request.HasHeaders && request.Headers.Host != null)
8890
{
8991
// Use the host name without any normalization.
9092
hostName = request.Headers.Host;

src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Authentication.cs

+49
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,55 @@ public static IEnumerable<object[]> ServerUsesWindowsAuthentication_MemberData()
525525
Configuration.Security.ActiveDirectoryUserPassword,
526526
Configuration.Security.ActiveDirectoryName);
527527

528+
public static IEnumerable<object[]> EchoServersData()
529+
{
530+
foreach (Uri serverUri in Configuration.Http.EchoServerList)
531+
{
532+
yield return new object[] { serverUri };
533+
}
534+
}
535+
536+
[MemberData(nameof(EchoServersData))]
537+
[ConditionalTheory(nameof(IsDomainJoinedServerAvailable))]
538+
public async Task Proxy_DomainJoinedProxyServerUsesKerberos_Success(Uri server)
539+
{
540+
// We skip the test unless it is running on a Windows client machine. That is because only Windows
541+
// automatically registers an SPN for HTTP/<hostname> of the machine. This will enable Kerberos to properly
542+
// work with the loopback proxy server.
543+
if (!PlatformDetection.IsWindows || !PlatformDetection.IsNotWindowsNanoServer)
544+
{
545+
throw new SkipTestException("Test can only run on domain joined Windows client machine");
546+
}
547+
548+
var options = new LoopbackProxyServer.Options { AuthenticationSchemes = AuthenticationSchemes.Negotiate };
549+
using (LoopbackProxyServer proxyServer = LoopbackProxyServer.Create(options))
550+
{
551+
using (HttpClientHandler handler = CreateHttpClientHandler())
552+
using (HttpClient client = CreateHttpClient(handler))
553+
{
554+
// Use 'localhost' DNS name for loopback proxy server (instead of IP address) so that the SPN will
555+
// get calculated properly to use Kerberos.
556+
_output.WriteLine(proxyServer.Uri.AbsoluteUri.ToString());
557+
handler.Proxy = new WebProxy("localhost", proxyServer.Uri.Port) { Credentials = DomainCredential };
558+
559+
using (HttpResponseMessage response = await client.GetAsync(server))
560+
{
561+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
562+
563+
int requestCount = proxyServer.Requests.Count;
564+
565+
// We expect 2 requests to the proxy server. One without the 'Proxy-Authorization' header and
566+
// one with the header.
567+
Assert.Equal(2, requestCount);
568+
Assert.Equal("Negotiate", proxyServer.Requests[requestCount - 1].AuthorizationHeaderValueScheme);
569+
570+
// Base64 tokens that use SPNEGO protocol start with 'Y'. NTLM tokens start with 'T'.
571+
Assert.Equal('Y', proxyServer.Requests[requestCount - 1].AuthorizationHeaderValueToken[0]);
572+
}
573+
}
574+
}
575+
}
576+
528577
[ConditionalFact(nameof(IsDomainJoinedServerAvailable))]
529578
public async Task Credentials_DomainJoinedServerUsesKerberos_Success()
530579
{

0 commit comments

Comments
 (0)