Add NTLM feature detection and more tests (dotnet/corefx#35200)
authorDavid Shulman <david.shulman@microsoft.com>
Sun, 10 Feb 2019 16:24:06 +0000 (08:24 -0800)
committerGitHub <noreply@github.com>
Sun, 10 Feb 2019 16:24:06 +0000 (08:24 -0800)
This PR works toward the goal of our Enterprise Scenarios. I added logic in the Unix
PAL gssapi layer to detect whether the NTLM plugin is installed. This is useful both
in the product code (to return better exception messages) and the test code (to
skip NTLM tests if needed).

Refactored some of the Common System.Net.Configuration logic for tests. Added new
environment variable logic to separate between domain-joined and standalone server
environments.

Add new tests to validate NTLM end-to-end connectivity. Opened some new issues
discovered along with way regarding HTTP/2 and Windows authentication.

To run these new tests simply set the 3 environment variables to point to the
standalone windows server environment.  For example, on Linux:

```
export COREFX_WINDOWSSERVER_HTTPHOST=somewindowsmachine.contoso.com
export COREFX_NET_SERVER_USERNAME=someusername
export COREFX_NET_SERVER_PASSWORD=somepassword
```

I have already deployed a standalone Windows server for these tests but
we're not ready yet to have the tests always run in CI.

Contributes to dotnet/corefx#34878

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

16 files changed:
src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.IsNtlmInstalled.cs [new file with mode: 0644]
src/libraries/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs
src/libraries/Common/tests/System/Net/Capability.Security.Unix.cs [new file with mode: 0644]
src/libraries/Common/tests/System/Net/Capability.Security.Windows.cs [new file with mode: 0644]
src/libraries/Common/tests/System/Net/Configuration.Http.cs
src/libraries/Common/tests/System/Net/Configuration.Security.cs
src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.c
src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.h
src/libraries/System.Data.SqlClient/src/System.Data.SqlClient.csproj
src/libraries/System.Net.Http/src/System.Net.Http.csproj
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Authentication.cs
src/libraries/System.Net.Http/tests/FunctionalTests/PlatformHandlerTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs
src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
src/libraries/System.Net.Mail/src/System.Net.Mail.csproj
src/libraries/System.Net.Security/src/System.Net.Security.csproj

diff --git a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.IsNtlmInstalled.cs b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.IsNtlmInstalled.cs
new file mode 100644 (file)
index 0000000..91d2d4f
--- /dev/null
@@ -0,0 +1,15 @@
+// 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.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+    internal static partial class NetSecurityNative
+    {
+        [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_IsNtlmInstalled")]
+        internal static extern bool IsNtlmInstalled();
+    }
+}
index e90b25f..0a10038 100644 (file)
@@ -72,12 +72,22 @@ namespace Microsoft.Win32.SafeHandles
     /// </summary>
     internal class SafeGssCredHandle : SafeHandle
     {
+        private static readonly Lazy<bool> s_IsNtlmInstalled = new Lazy<bool>(InitIsNtlmInstalled);
+
         /// <summary>
         ///  returns the handle for the given credentials.
         ///  The method returns an invalid handle if the username is null or empty.
         /// </summary>
         public static SafeGssCredHandle Create(string username, string password, bool isNtlmOnly)
         {
+            if (isNtlmOnly && !s_IsNtlmInstalled.Value)
+            {
+                throw new Interop.NetSecurityNative.GssApiException(
+                    Interop.NetSecurityNative.Status.GSS_S_BAD_MECH,
+                    0,
+                    SR.net_gssapi_ntlm_missing_plugin);
+            }
+
             if (string.IsNullOrEmpty(username))
             {
                 return new SafeGssCredHandle();
@@ -100,12 +110,7 @@ namespace Microsoft.Win32.SafeHandles
                 if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE)
                 {
                     retHandle.Dispose();
-                    throw new Interop.NetSecurityNative.GssApiException(
-                        status,
-                        minorStatus,
-                        (status == Interop.NetSecurityNative.Status.GSS_S_BAD_MECH && isNtlmOnly) ?
-                            SR.net_gssapi_ntlm_missing_plugin :
-                            null);
+                    throw new Interop.NetSecurityNative.GssApiException(status, minorStatus, null);
                 }
             }
 
@@ -129,6 +134,11 @@ namespace Microsoft.Win32.SafeHandles
             SetHandle(IntPtr.Zero);
             return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE;
         }
+
+        private static bool InitIsNtlmInstalled()
+        {
+            return Interop.NetSecurityNative.IsNtlmInstalled();
+        }
     }
 
     internal sealed class SafeGssContextHandle : SafeHandle
diff --git a/src/libraries/Common/tests/System/Net/Capability.Security.Unix.cs b/src/libraries/Common/tests/System/Net/Capability.Security.Unix.cs
new file mode 100644 (file)
index 0000000..3198bc2
--- /dev/null
@@ -0,0 +1,14 @@
+// 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.
+
+namespace System.Net.Test.Common
+{
+    public static partial class Capability
+    {
+        public static bool IsNtlmInstalled()
+        {
+            return Interop.NetSecurityNative.IsNtlmInstalled();
+        }
+    }
+}
diff --git a/src/libraries/Common/tests/System/Net/Capability.Security.Windows.cs b/src/libraries/Common/tests/System/Net/Capability.Security.Windows.cs
new file mode 100644 (file)
index 0000000..41a0ebf
--- /dev/null
@@ -0,0 +1,14 @@
+// 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.
+
+namespace System.Net.Test.Common
+{
+    public static partial class Capability
+    {
+        public static bool IsNtlmInstalled()
+        {
+            return true;
+        }
+    }
+}
index 27d70db..5e305b9 100644 (file)
@@ -23,12 +23,14 @@ namespace System.Net.Test.Common
             // don't support servers that use push right now.
             public static string Http2NoPushHost => GetValue("COREFX_HTTP2NOPUSHHOST", "www.microsoft.com");
 
+            // Domain server environment.
             public static string DomainJoinedHttpHost => GetValue("COREFX_DOMAINJOINED_HTTPHOST");
-
             public static string DomainJoinedProxyHost => GetValue("COREFX_DOMAINJOINED_PROXYHOST");
-
             public static string DomainJoinedProxyPort => GetValue("COREFX_DOMAINJOINED_PROXYPORT");
 
+            // Standalone server environment.
+            public static string WindowsServerHttpHost => GetValue("COREFX_WINDOWSSERVER_HTTPHOST");
+
             public static string SSLv2RemoteServer => GetValue("COREFX_HTTPHOST_SSL2", "https://www.ssllabs.com:10200/");
             public static string SSLv3RemoteServer => GetValue("COREFX_HTTPHOST_SSL3", "https://www.ssllabs.com:10300/");
             public static string TLSv10RemoteServer => GetValue("COREFX_HTTPHOST_TLS10", "https://www.ssllabs.com:10301/");
index b5a3e5c..b045b1c 100644 (file)
@@ -10,12 +10,16 @@ namespace System.Net.Test.Common
         {
             private static readonly string DefaultAzureServer = "corefx-net.cloudapp.net";
 
+            // Domain server environment.
             public static string ActiveDirectoryName => GetValue("COREFX_NET_AD_DOMAINNAME");
-
             public static string ActiveDirectoryUserName => GetValue("COREFX_NET_AD_USERNAME");
-
             public static string ActiveDirectoryUserPassword => GetValue("COREFX_NET_AD_PASSWORD");
 
+            // Standalone server environment.
+            public static string WindowsServerRealmName => GetValue("COREFX_NET_SERVER_REALMNAME");
+            public static string WindowsServerUserName => GetValue("COREFX_NET_SERVER_USERNAME");
+            public static string WindowsServerUserPassword => GetValue("COREFX_NET_SERVER_PASSWORD");
+
             public static Uri TlsServer => GetUriValue("COREFX_NET_SECURITY_TLSSERVERURI", new Uri("https://" + DefaultAzureServer));
 
             public static Uri NegotiateServer => GetUriValue("COREFX_NET_SECURITY_NEGOSERVERURI");
index 231e35b..9c5ef4c 100644 (file)
@@ -427,3 +427,36 @@ uint32_t NetSecurityNative_InitiateCredWithPassword(uint32_t* minorStatus,
     return NetSecurityNative_AcquireCredWithPassword(
         minorStatus, isNtlm, desiredName, password, passwdLen, GSS_C_INITIATE, outputCredHandle);
 }
+
+uint32_t NetSecurityNative_IsNtlmInstalled()
+{
+#if HAVE_GSS_SPNEGO_MECHANISM
+    gss_OID ntlmOid = GSS_NTLM_MECHANISM;
+#else
+    gss_OID ntlmOid = &gss_mech_ntlm_OID_desc;
+#endif
+
+    uint32_t majorStatus;
+    uint32_t minorStatus;
+    gss_OID_set mechSet;
+    gss_OID_desc oid;
+    uint32_t foundNtlm = 0;
+
+    majorStatus = gss_indicate_mechs(&minorStatus, &mechSet);
+    if (majorStatus == GSS_S_COMPLETE)
+    {
+        for (size_t i = 0; i < mechSet->count; i++)
+        {
+            oid = mechSet->elements[i];
+            if ((oid.length == ntlmOid->length) && (memcmp(oid.elements, ntlmOid->elements, oid.length) == 0))
+            {
+                foundNtlm = 1;
+                break;
+            }
+        }
+
+        gss_release_oid_set(&minorStatus, &mechSet);
+    }
+
+    return foundNtlm;
+}
index b2c911a..4203916 100644 (file)
@@ -162,3 +162,8 @@ DLLEXPORT uint32_t NetSecurityNative_InitiateCredWithPassword(uint32_t* minorSta
                                                               char* password,
                                                               uint32_t passwdLen,
                                                               GssCredId** outputCredHandle);
+
+/*
+Shims the gss_indicate_mechs method to detect if NTLM mech is installed.
+*/
+DLLEXPORT uint32_t NetSecurityNative_IsNtlmInstalled(void);
index edd0a65..0058e7c 100644 (file)
     <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs">
       <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs">
+      <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)\System\Net\Security\Unix\SafeFreeNegoCredentials.cs">
       <Link>Common\System\Net\Security\Unix\SafeFreeNegoCredentials.cs</Link>
     </Compile>
index 837c2c6..d01b16d 100644 (file)
     <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs">
       <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs">
+      <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.GssBuffer.cs">
       <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.GssBuffer.cs</Link>
     </Compile>
index f95a2fc..1cf5877 100644 (file)
@@ -8,12 +8,17 @@ using System.Text;
 using System.Threading.Tasks;
 
 using Xunit;
+using Xunit.Abstractions;
 
 namespace System.Net.Http.Functional.Tests
 {
+    using Configuration = System.Net.Test.Common.Configuration;
+
     [SkipOnTargetFramework(TargetFrameworkMonikers.Uap, "Tests would need to be rewritten due to behavior differences with WinRT")]
     public abstract class HttpClientHandler_Authentication_Test : HttpClientHandlerTestBase
     {
+        private readonly ITestOutputHelper _output;
+
         private const string Username = "testusername";
         private const string Password = "testpassword";
         private const string Domain = "testdomain";
@@ -32,6 +37,11 @@ namespace System.Net.Http.Functional.Tests
             }
         };
 
+        public HttpClientHandler_Authentication_Test(ITestOutputHelper output)
+        {
+            _output = output;
+        }
+
         [Theory]
         [MemberData(nameof(Authentication_TestData))]
         public async Task HttpClientHandler_Authentication_Succeeds(string authenticateHeader, bool result)
@@ -490,5 +500,51 @@ namespace System.Net.Http.Functional.Tests
                 Assert.Contains(headers, header => header.Contains("Authorization: Digest"));
             });
         }
+
+        public static IEnumerable<object[]> ServerUsesWindowsAuthentication_MemberData()
+        {
+            string server = Configuration.Http.WindowsServerHttpHost;
+            string authEndPoint = "showidentity.ashx";
+
+            yield return new object[] { $"http://{server}/test/auth/ntlm/{authEndPoint}", false };
+            yield return new object[] { $"https://{server}/test/auth/ntlm/{authEndPoint}", false };
+
+            // Server requires TLS channel binding token (cbt) with NTLM authentication.
+            // CurlHandler (due to libcurl bug) cannot do NTLM authentication with cbt.
+            yield return new object[] { $"https://{server}/test/auth/ntlm-epa/{authEndPoint}", true };
+        }
+
+        private static bool IsNtlmInstalled => Capability.IsNtlmInstalled();
+        private static bool IsWindowsServerAvailable => !string.IsNullOrEmpty(Configuration.Http.WindowsServerHttpHost);
+
+        [ConditionalTheory(nameof(IsNtlmInstalled), nameof(IsWindowsServerAvailable))]
+        [MemberData(nameof(ServerUsesWindowsAuthentication_MemberData))]
+        public async Task Credentials_ServerUsesWindowsAuthentication_Success(string server, bool skipOnCurlHandler)
+        {
+            if (IsCurlHandler && skipOnCurlHandler) return;
+
+            using (HttpClientHandler handler = CreateHttpClientHandler())
+            using (var client = new HttpClient(handler))
+            {
+                handler.Credentials = new NetworkCredential(
+                    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))
+                {
+                    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+                    string body = await response.Content.ReadAsStringAsync();
+                    _output.WriteLine(body);
+                }
+            }
+        }
     }
 }
index e618b1d..28d3515 100644 (file)
@@ -151,6 +151,7 @@ namespace System.Net.Http.Functional.Tests
 
     public sealed class PlatformHandler_HttpClientHandler_Authentication_Test : HttpClientHandler_Authentication_Test
     {
+        public PlatformHandler_HttpClientHandler_Authentication_Test(ITestOutputHelper output) : base(output) { }
         protected override bool UseSocketsHttpHandler => false;
     }
 
index 0d2bc7e..6495435 100644 (file)
@@ -685,6 +685,7 @@ namespace System.Net.Http.Functional.Tests
 
     public sealed class SocketsHttpHandler_HttpClientHandler_Authentication_Test : HttpClientHandler_Authentication_Test
     {
+        public SocketsHttpHandler_HttpClientHandler_Authentication_Test(ITestOutputHelper output) : base(output) { }
         protected override bool UseSocketsHttpHandler => true;
 
         [Theory]
index a752414..847649e 100644 (file)
@@ -13,6 +13,9 @@
     <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Http.Native\Interop.VersionInfo.cs" Condition="'$(TargetsUnix)' == 'true'">
       <Link>Common\Interop\Unix\System.Net.Http.Native\Interop.VersionInfo.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs" Condition="'$(TargetsUnix)' == 'true'">
+      <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs</Link>
+    </Compile>
     <Compile Include="$(CommonTestPath)\System\Buffers\NativeMemoryManager.cs">
       <Link>Common\System\Buffers\NativeMemoryManager.cs</Link>
     </Compile>
     <Compile Include="$(CommonTestPath)\System\Net\Capability.Security.cs">
       <Link>Common\System\Net\Capability.Security.cs</Link>
     </Compile>
+    <Compile Include="$(CommonTestPath)\System\Net\Capability.Security.Windows.cs" Condition="'$(TargetsWindows)' == 'true'">
+      <Link>Common\System\Net\Capability.Security.Windows.cs</Link>
+    </Compile>
+    <Compile Include="$(CommonTestPath)\System\Net\Capability.Security.Unix.cs" Condition="'$(TargetsUnix)' == 'true'">
+      <Link>Common\System\Net\Capability.Security.Unix.cs</Link>
+    </Compile>
     <Compile Include="$(CommonTestPath)\System\Net\Configuration.cs">
       <Link>Common\System\Net\Configuration.cs</Link>
     </Compile>
index 53c19af..5b84616 100644 (file)
     <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs">
       <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs">
+      <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)\System\Net\Security\Unix\SafeDeleteNegoContext.cs">
       <Link>Common\System\Net\Security\Unix\SafeDeleteNegoContext.cs</Link>
     </Compile>
index ad5c116..7dbe47d 100644 (file)
     <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs">
       <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs">
+      <Link>Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.IsNtlmInstalled.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)\System\Net\ContextFlagsAdapterPal.Unix.cs">
       <Link>Common\System\Net\ContextFlagsAdapterPal.Unix.cs</Link>
     </Compile>