runtime: .NET Core 2.1 SocketsHttpHandler does not use Negotiate / SPNego

Overview

While testing PowerShell Core 6.1 I ran into an issue with not being able to authenticate to a Kerberized REST API running on Linux unless I disable SocketsHttpHandler. https://github.com/PowerShell/PowerShell/issues/7801

It seems like that when the server responds with both Negotiate and NTLM, the SocketsHttpHandler picks NTLM which in my case results in a 401 as the service in question is really expecting Negotiate / SPNego and is not working with NTLM.

As requested by @karelz in https://github.com/dotnet/corefx/issues/30166 I’ve reproduced it on the daily builds without PowerShell Core involved and same results, so submitting a new issue for this.

Expected result

When server sends multiple auth schemes like Negotiate and NTLM, pick the strongest one which in this case is Negotiate.

Dotnet Info

C:\dev\test\httpclient-spnego\test2>dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   2.1.403-servicing-009270
 Commit:    def6c5f48d

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.15063
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\2.1.403-servicing-009270\

Host (useful for support):
  Version: 2.1.5-servicing-26911-03
  Commit:  efdba896f7

.NET Core SDKs installed:
  2.1.201-preview-007614 [C:\Program Files\dotnet\sdk]
  2.1.202 [C:\Program Files\dotnet\sdk]
  2.1.400 [C:\Program Files\dotnet\sdk]
  2.1.402 [C:\Program Files\dotnet\sdk]
  2.1.403-servicing-009270 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.5-rtm-31008 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.5-rtm-31008 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.0.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.5-servicing-26911-03 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]

To install additional .NET Core runtimes or SDKs:
  https://aka.ms/dotnet-download

Example repro

var handler = new HttpClientHandler
{
    UseDefaultCredentials = true,
    AllowAutoRedirect = true,
};
using (var client = new HttpClient(handler))
{
    var res = client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri)).GetAwaiter().GetResult();
    System.Console.WriteLine(res);
}

Result: 401

HTTP traffic from packet capture

GET / HTTP/1.1 Host: mykerberossite.lab.local

HTTP/1.1 401 Unauthorized Date: Mon, 17 Sep 2018 21:31:42 GMT Server: Apache-Coyote/1.1 WWW-Authenticate: Negotiate WWW-Authenticate: NTLM Content-Length: 0

GET / HTTP/1.1 Authorization: Negotiate **** Host: mykerberossite.lab.local

HTTP/1.1 401 Unauthorized Date: Mon, 17 Sep 2018 21:31:42 GMT Server: Apache-Coyote/1.1 WWW-Authenticate: NTLM Content-Length: 0

Workaround is to disable SocketsHttpHandler

AppContext.SetSwitch("System.Net.Http.UseSocketsHttpHandler", false);
var handler = new HttpClientHandler
{
    UseDefaultCredentials = true,
    AllowAutoRedirect = true,

};
using (var client = new HttpClient(handler))
{
    var res = client.SendAsync(new HttpRequestMessage(HttpMethod.Get, uri)).GetAwaiter().GetResult();
    System.Console.WriteLine(res);
}

result: 200

HTTP Traffic

GET / HTTP/1.1 Connection: Keep-Alive Host: mykerberosite.lab.local

HTTP/1.1 401 Unauthorized Date: Mon, 17 Sep 2018 21:30:27 GMT Server: Apache-Coyote/1.1 WWW-Authenticate: Negotiate WWW-Authenticate: NTLM Content-Length: 0 Keep-Alive: timeout=5, max=100 Connection: Keep-Alive

GET / HTTP/1.1 Connection: Keep-Alive Host: mykerberossite.lab.local Authorization: Negotiate ***

HTTP/1.1 200 OK Date: Mon, 17 Sep 2018 21:30:27 GMT Server: Apache-Coyote/1.1 WWW-Authenticate: Negotiate *** Cache-Control: no-cache Expires: -1 Content-Type: text/plain;charset=UTF-8 Content-Length: 103 Keep-Alive: timeout=5, max=99 Connection: Keep-Alive

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 37 (21 by maintainers)

Commits related to this issue

Most upvoted comments

fyi. I’ll be OOF for about a week+. So, I’ll be submitting the PR for this fix as soon as I get back.

It’s hard for me to tell the impact of this, but surely impacts all shops using Negotiate with CNames. Plus this is a breaking change for Negotiate auth introduced with SocketsHttpHandler requiring a workaround to disable it and loosing on perf.

The workaround is to either use the DNS A record or disable SocketsHttpHandler (more preferable in cases when CNames can change).

AppContext.SetSwitch("System.Net.Http.UseSocketsHttpHandler", false);

or set the env var

$env:DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER=0

Once set, these settings have a potential to be easily forgotten to be undone by dev teams after moving to 3.0 and missing out on the new perf improvements (unless these settings will be ignored with 3.0).

Thank you for your fix @davidsh !

Just wondering if it would be possible to backport this merge request to either 2.2 or one of it’s servicing releases so this fix become available for 2.2 as well and for PowerShell Core 6.2 ?

So this will ship with 3.0 ?

Yes, the fix is in the master branch for 3.0.

so the most reasonable thing to do is to just do what IE did way back when and the other browsers subsequently emulated - forward canoncialize, no reverse

This is what the current .NET Framework behavior is. And this is what the fix for .NET Core will be also.

I was able to research this problem with a Windows-Windows setup in our separate Enterprise Testing environment.

Given an IIS server called “corefx-net-iis” on a domain called “corefx-net.contoso.com”, we are able to get Negotiate to use Kerberos with using any of the following URI’s.

// Use A record of server
string server = "http://corefx-net-iis/test/NegotiateTest.ashx";
string server = "http://corefx-net-iis.corefx-net.contoso.com/test/NegotiateTest.ashx";

// Use CNAME of server
string server = "http://iis-server/test/NegotiateTest.ashx";
string server = "http://iis-server.corefx-net.contoso.com/test/NegotiateTest.ashx";

“iis-server.corefx-net.contoso.com” is a CNAME.

But for .NET Core 2.1.5, Negotiate will only use Kerberos when using the original FQDN of the server (A record):

string server = "http://corefx-net-iis/test/NegotiateTest.ashx";
string server = "http://corefx-net-iis.corefx-net.contoso.com/test/NegotiateTest.ashx";

Any of the DNS names using the CNAME results in Negotiate using NTLM.

Almost. That would handle CNAMEs and partially qualified names of As (that become fully qualified when the OS resolver appends one of the configured search suffixes). But…

  • If you pass an IP to GetHostEntry(), it will reverse it with a PTR lookup. I believe this behavior would be unexpected. Would probably want to be conditional on a failing IPAddress.TryParse.
  • GetHostEntry() will potentially use legacy and broadcast based name resolution protocols on Windows. These are not useful for canonicalization and slow down negative responses. A potential optimization would be to pass the relevant flags to the underlying Win32 APIs to not consider LLMNR, NetBIOS.