Use HTTP Host header for Kerberos auth SPN calculation (dotnet/corefx#38465)
authorDavid Shulman <david.shulman@microsoft.com>
Wed, 12 Jun 2019 02:53:55 +0000 (19:53 -0700)
committerGitHub <noreply@github.com>
Wed, 12 Jun 2019 02:53:55 +0000 (19:53 -0700)
Fixed SocketsHttpHandler so that it will use the request's Host header,
if present, as part of building the Service Principal Name (SPN) when
doing Kerberos authentication. This now matches .NET Framework behavior.

Contributes to dotnet/corefx#34697 and dotnet/corefx#27745

Commit migrated from https://github.com/dotnet/corefx/commit/6cb854630083aac733d5b8001b266b872b56748e

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Authentication.cs

index 3d3eaed..e973c3e 100644 (file)
@@ -76,32 +76,50 @@ namespace System.Net.Http
                             needDrain = false;
                         }
 
-                        string challengeData = challenge.ChallengeData;
+                        if (NetEventSource.IsEnabled)
+                        {
+                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Uri: {authUri.AbsoluteUri.ToString()}");
+                        }
 
-                        // Need to use FQDN normalized host so that CNAME's are traversed.
-                        // Use DNS to do the forward lookup to an A (host) record.
-                        // But skip DNS lookup on IP literals. Otherwise, we would end up
-                        // doing an unintended reverse DNS lookup.
-                        string spn;
-                        UriHostNameType hnt = authUri.HostNameType;
-                        if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4)
+                        // 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.
+                        string hostName;
+                        if (request.HasHeaders && request.Headers.Host != null)
                         {
-                            spn = authUri.IdnHost;
+                            // Use the host name without any normalization.
+                            hostName = request.Headers.Host;
+                            if (NetEventSource.IsEnabled)
+                            {
+                                NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: {hostName}");
+                            }
                         }
                         else
                         {
-                            IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost).ConfigureAwait(false);
-                            spn = result.HostName;
+                            // Need to use FQDN normalized host so that CNAME's are traversed.
+                            // Use DNS to do the forward lookup to an A (host) record.
+                            // But skip DNS lookup on IP literals. Otherwise, we would end up
+                            // doing an unintended reverse DNS lookup.
+                            UriHostNameType hnt = authUri.HostNameType;
+                            if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4)
+                            {
+                                hostName = authUri.IdnHost;
+                            }
+                            else
+                            {
+                                IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost).ConfigureAwait(false);
+                                hostName = result.HostName;
+                            }
                         }
-                        spn = "HTTP/" + spn;
 
+                        string spn = "HTTP/" + hostName;
                         if (NetEventSource.IsEnabled)
                         {
-                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: {authUri.IdnHost}, SPN: {spn}");
+                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, SPN: {spn}");
                         }
 
                         ChannelBinding channelBinding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint);
                         NTAuthentication authContext = new NTAuthentication(isServer:false, challenge.SchemeName, challenge.Credential, spn, ContextFlagsPal.Connection, channelBinding);
+                        string challengeData = challenge.ChallengeData;
                         try
                         {
                             while (true)
index 7b793a2..767b014 100644 (file)
@@ -3,6 +3,8 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Collections.Generic;
+using System.Linq;
+using System.Net.Sockets;
 using System.Net.Test.Common;
 using System.Text;
 using System.Threading.Tasks;
@@ -518,6 +520,10 @@ namespace System.Net.Http.Functional.Tests
         private static bool IsNtlmInstalled => Capability.IsNtlmInstalled();
         private static bool IsWindowsServerAvailable => !string.IsNullOrEmpty(Configuration.Http.WindowsServerHttpHost);
         private static bool IsDomainJoinedServerAvailable => !string.IsNullOrEmpty(Configuration.Http.DomainJoinedHttpHost);
+        private static NetworkCredential DomainCredential = new NetworkCredential(
+                    Configuration.Security.ActiveDirectoryUserName,
+                    Configuration.Security.ActiveDirectoryUserPassword,
+                    Configuration.Security.ActiveDirectoryName);
 
         [ConditionalFact(nameof(IsDomainJoinedServerAvailable))]
         public async Task Credentials_DomainJoinedServerUsesKerberos_Success()
@@ -530,19 +536,39 @@ namespace System.Net.Http.Functional.Tests
             using (HttpClientHandler handler = CreateHttpClientHandler())
             using (HttpClient client = CreateHttpClient(handler))
             {
-                handler.Credentials = new NetworkCredential(
-                    Configuration.Security.ActiveDirectoryUserName,
-                    Configuration.Security.ActiveDirectoryUserPassword,
-                    Configuration.Security.ActiveDirectoryName);
+                handler.Credentials = DomainCredential;
 
-                var request = new HttpRequestMessage();
-                var server = $"http://{Configuration.Http.DomainJoinedHttpHost}/test/auth/kerberos/showidentity.ashx";
-                request.RequestUri = new Uri(server);
+                string server = $"http://{Configuration.Http.DomainJoinedHttpHost}/test/auth/kerberos/showidentity.ashx";
+                using (HttpResponseMessage response = await client.GetAsync(server))
+                {
+                    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                    string body = await response.Content.ReadAsStringAsync();
+                    _output.WriteLine(body);
+                }
+            }
+        }
 
-                // Force HTTP/1.1 since both CurlHandler and SocketsHttpHandler have problems with
-                // HTTP/2.0 and Windows authentication (due to HTTP/2.0 -> HTTP/1.1 downgrade handling).
-                // Issue #35195 (for SocketsHttpHandler).
-                request.Version = new Version(1,1);
+        [ConditionalFact(nameof(IsDomainJoinedServerAvailable))]
+        public async Task Credentials_DomainJoinedServerUsesKerberos_UseIpAddressAndHostHeader_Success()
+        {
+            if (IsCurlHandler || IsWinHttpHandler)
+            {
+                throw new SkipTestException("Skipping test on platform handlers (CurlHandler, WinHttpHandler)");
+            }
+
+            using (HttpClientHandler handler = CreateHttpClientHandler())
+            using (HttpClient client = CreateHttpClient(handler))
+            {
+                handler.Credentials = DomainCredential;
+
+                IPAddress[] addresses = Dns.GetHostAddresses(Configuration.Http.DomainJoinedHttpHost);
+                IPAddress hostIP = addresses.Where(a => a.AddressFamily == AddressFamily.InterNetwork).Select(a => a).First();
+
+                var request = new HttpRequestMessage();
+                request.RequestUri = new Uri($"http://{hostIP}/test/auth/kerberos/showidentity.ashx");
+                request.Headers.Host = Configuration.Http.DomainJoinedHttpHost;
+                _output.WriteLine(request.RequestUri.AbsoluteUri.ToString());
+                _output.WriteLine($"Host: {request.Headers.Host}");
 
                 using (HttpResponseMessage response = await client.SendAsync(request))
                 {
@@ -569,15 +595,7 @@ namespace System.Net.Http.Functional.Tests
                     Configuration.Security.WindowsServerUserName,
                     Configuration.Security.WindowsServerUserPassword);
 
-                var request = new HttpRequestMessage();
-                request.RequestUri = new Uri(server);
-
-                // Force HTTP/1.1 since both CurlHandler and SocketsHttpHandler have problems with
-                // HTTP/2.0 and Windows authentication (due to HTTP/2.0 -> HTTP/1.1 downgrade handling).
-                // Issue #35195 (for SocketsHttpHandler).
-                request.Version = new Version(1,1);
-
-                using (HttpResponseMessage response = await client.SendAsync(request))
+                using (HttpResponseMessage response = await client.GetAsync(server))
                 {
                     Assert.Equal(HttpStatusCode.OK, response.StatusCode);
                     string body = await response.Content.ReadAsStringAsync();