diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs index e973c3e86083..bb39d675e728 100644 --- a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs @@ -83,8 +83,10 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe // Calculate SPN (Service Principal Name) using the host name of the request. // Use the request's 'Host' header if available. Otherwise, use the request uri. + // Ignore the 'Host' header if this is proxy authentication since we need to use + // the host name of the proxy itself for SPN calculation. string hostName; - if (request.HasHeaders && request.Headers.Host != null) + if (!isProxyAuth && request.HasHeaders && request.Headers.Host != null) { // Use the host name without any normalization. hostName = request.Headers.Host; diff --git a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Authentication.cs b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Authentication.cs index f296a2e68f74..a44b5bac268d 100644 --- a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Authentication.cs +++ b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Authentication.cs @@ -524,6 +524,55 @@ public static IEnumerable ServerUsesWindowsAuthentication_MemberData() Configuration.Security.ActiveDirectoryUserPassword, Configuration.Security.ActiveDirectoryName); + public static IEnumerable EchoServersData() + { + foreach (Uri serverUri in Configuration.Http.EchoServerList) + { + yield return new object[] { serverUri }; + } + } + + [MemberData(nameof(EchoServersData))] + [ConditionalTheory(nameof(IsDomainJoinedServerAvailable))] + public async Task Proxy_DomainJoinedProxyServerUsesKerberos_Success(Uri server) + { + // We skip the test unless it is running on a Windows client machine. That is because only Windows + // automatically registers an SPN for HTTP/ of the machine. This will enable Kerberos to properly + // work with the loopback proxy server. + if (!PlatformDetection.IsWindows || !PlatformDetection.IsNotWindowsNanoServer) + { + throw new SkipTestException("Test can only run on domain joined Windows client machine"); + } + + var options = new LoopbackProxyServer.Options { AuthenticationSchemes = AuthenticationSchemes.Negotiate }; + using (LoopbackProxyServer proxyServer = LoopbackProxyServer.Create(options)) + { + using (HttpClientHandler handler = CreateHttpClientHandler()) + using (HttpClient client = CreateHttpClient(handler)) + { + // Use 'localhost' DNS name for loopback proxy server (instead of IP address) so that the SPN will + // get calculated properly to use Kerberos. + _output.WriteLine(proxyServer.Uri.AbsoluteUri.ToString()); + handler.Proxy = new WebProxy("localhost", proxyServer.Uri.Port) { Credentials = DomainCredential }; + + using (HttpResponseMessage response = await client.GetAsync(server)) + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + int requestCount = proxyServer.Requests.Count; + + // We expect 2 requests to the proxy server. One without the 'Proxy-Authorization' header and + // one with the header. + Assert.Equal(2, requestCount); + Assert.Equal("Negotiate", proxyServer.Requests[requestCount - 1].AuthorizationHeaderValueScheme); + + // Base64 tokens that use SPNEGO protocol start with 'Y'. NTLM tokens start with 'T'. + Assert.Equal('Y', proxyServer.Requests[requestCount - 1].AuthorizationHeaderValueToken[0]); + } + } + } + } + [ConditionalFact(nameof(IsDomainJoinedServerAvailable))] public async Task Credentials_DomainJoinedServerUsesKerberos_Success() {