runtime: HttpListener sends malformed WWW-Authenticate mutual auth header
Hope this is the right place to file this, not clear to me if .NET Framework issues are in scope for this repo.
In the RFC for SPNEGO in HTTP, section 5 shows the expected exchange. In the final 200 response from the server, the server sets a WWW-Authenticate header to Negoitate base64(gssapi-data)
. HttpListener sets this header to only base64(gssapi-data)
. For other Windows .NET clients, this seems to be accepted, but things that have a stricter interpretation of the spec, such as the requests-kerberos Python library, it doesn’t fly.
I’m pretty deeply confused about this because generally speaking, my understanding is that mutual auth and Negotiate in general is more or less http.sys’s responsibility. With stuff hosted in IIS, I see the header being set with the appropriate Negotiate
prefix. I threw together a very simple .NET Core app using HttpSysListener, and it also sets the header correctly. I even hacked some C sample code for the HTTP Server API to enable Negotiate and send 401s as necessary, and it also sets the header correctly. The only thing that doesn’t is HttpListener. I have not had much luck digging through the source to figure out where this might be going wrong. In my actual app, I even tried re-writing the header myself in some Owin middleware, but the actual response on the wire was still wrong.
To repro…
- Get a pair of Windows machines joined to the same domain, or with a trust in between them.
- Machine A must be reachable by FQDN, that and the machine account must have the
HTTP/fqdn
SPN attached in AD. Unless you explicitly assign the HTTP SPN to something, it’s implicit in the HOST SPN, and you get that by default when the machine is joined to the domain, so you probably don’t need to do anything here. - On machine A, install .NET 4.7.1. The error is definitely there in 4.7.1, have not tested earlier frameworks.
- On machine A, make sure incoming TCP/9002 (or whatever port you want to use) is open in the firewall.
- On machine A, use
psexec -i -s cmd
to get a shell as SYSTEM. This is a quick and dirty way to act with the computer’s credentials, avoid the need to register the prefix with netsh. - Build the code below and run it in that SYSTEM shell on A.
- On B, as a domain user, hit the test path on machine A form something that will do Negotiate. You must use the name, not IP and have connectivity to a DC so that Kerberos works. This PowerShell one-liner will do the trick:
(Invoke-WebRequest http://machinea.example.com:9002/test -UseDefaultCredentials -UseBasicParsing).RawContent
- Observe headers that look something like this. Note the lack of Negotiate prefix in the WWW-Authenticate header.
HTTP/1.1 200 OK
Content-Length: 11
Date: Fri, 26 Jan 2018 20:53:23 GMT
Server: Microsoft-HTTPAPI/2.0
WWW-Authenticate: oYG3MIG0oAMKAQChCwYJKoZIgvcSAQICooG <more base64 snipped>
Hello world
Sample code:
using System;
using System.Net;
using System.Text;
namespace HttpListenerTest
{
class Program
{
static void Main()
{
try
{
var responseString = "Hello world";
var buffer = Encoding.UTF8.GetBytes(responseString);
var listener = new HttpListener {AuthenticationSchemes = AuthenticationSchemes.Negotiate};
var prefix = "http://+:9002/test/";
listener.Prefixes.Add(prefix);
listener.Start();
Console.WriteLine($"Listening on {prefix}...");
while (true)
{
var context = listener.GetContext();
var request = context.Request;
Console.WriteLine($"Hit from {request.RemoteEndPoint.Address}...");
var response = context.Response;
response.ContentLength64 = buffer.Length;
var output = response.OutputStream;
output.Write(buffer, 0, buffer.Length);
output.Close();
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Comments: 22 (22 by maintainers)
Commits related to this issue
- Prefix WWW-Authenticate header with scheme per RFC 4559 Fixes https://github.com/dotnet/corefx/issues/26606 — committed to mattpwhite/corefx by deleted user 6 years ago
- Prefix WWW-Authenticate header with scheme per RFC 4559 Fixes https://github.com/dotnet/corefx/issues/26606 — committed to mattpwhite/corefx by deleted user 6 years ago
- Prefix WWW-Authenticate header with scheme per RFC 4559 Fixes https://github.com/dotnet/corefx/issues/26606 — committed to mattpwhite/corefx by deleted user 6 years ago
- Prefix WWW-Authenticate header with scheme per RFC 4559 Fixes https://github.com/dotnet/corefx/issues/26606 — committed to mattpwhite/corefx by deleted user 6 years ago
- Prefix WWW-Authenticate header with scheme per RFC 4559 (#27755) Fixes: #26606 — committed to dotnet/corefx by mattpwhite 6 years ago
OK, think I found it.
HttpListener
is explicitly writing theWWW-Authenticate
headers in responses to clients. It appears based on my testing that http.sys will tag mutual auth data onto an outgoing response if noWWW-Authenticate
header is set by user mode, but if one is, it will leave it as is. That’s all fine, provided HttpListener puts the right data in the header, but it does not. It calls into SSPI and takes the output buffer fromAcceptSecurityContext()
, base64s it, and puts that base64 into the header value, without the authentication scheme prefix. The base64 token is correct, the SSPI call is the right one, the issue is in formatting the (not HTTP specific) binary token for use with HTTP.The flow is:
GetOutgoingBlob
helper method inNTAuthentication.Common
with the binary tokenGetOutgoingBlob
passes the binary to the nativeAcceptSecurityContext
function and gets a binary response along with an error code indicating whether we are done or have more rounds to do. If we’re done, and Kerberos is the underlying mechanism, then this binary is the mutual auth token. NTLM doesn’t support mutual auth, so presumably it’s null in that case.GetOutgoingBlob
returns the binary response.HttpListenerContext
and stored in a_mutualAuthentication
field.MutualAuthentication
property that exposes that field is checked, and if non-null, aWWW-Authenticate
header with its value is added.Someone in steps 6-8 needs to prefix the value that is ultimately going to be written to the header. Or maybe steps 6-8 shouldn’t exist at all, and http.sys should be relied upon to put the header in. That’s simpler, but if you ever wanted this to support Kerberos on non-Windows platforms, having the managed code make the platform specific native calls probably makes more sense (there are GSSAPI equivalents of all of the Windows APIs being used here,
gss_accept_sec_context
would replaceAcceptSecurityContext
and so on).I have a patch that I think should do the trick, managed to figure out how to get a sample app to use a private corefx and verified that it doesn’t break existing .NET clients, and now supports the Python clients that caused me to log this issue. I need approval from my employer before I send this over and accept what the agreement though, hopefully soon.
One distressing thing I discovered in the course of testing this is that
HttpWebRequest
seems like it might be lying when it says it implements mutual authentication. TheHttpWebResponse
will tell you that you’re mutually authenticated, regardless of what the server puts in the 200 WWW-Authenticate, so long as Kerberos is used for client authentication. If NTLM is used it will say no, and if you setAuthenticationLevel
toMutualAuthRequired
, it will throw if NTLM is used, but that’s it. I modified my little C http.sys server to set the header to literally “garbage” and it’s totally happy with that. The server proves its identity to the client by setting WWW-Authenticate to a value that proves that it was able to decrypt the session key that the client sent it. Without that, mutual authentication hasn’t happened.HttpClient (in core or the framework) doesn’t seem to make any promises about mutual auth one way or the other, so it’s excusable there.
FWIW, the
HttpWebRequest
issue is not really a concern for me, I just thought I should mention it. My reason for pursuing any of this was that I wanted clients that do implement the spec to be able to talk to my service. Actually relying on Kerberos (only) for mutual auth in the context of HTTP is a dicey thing to do in general, it’s vastly easier (not to mention the privacy and integrity) to say that server auth happens with TLS (so it happens before you start talking).Here’s another reference for you that the Negotiate scheme prefix is in fact required. This predates (appears to be written in 2002) the RFC because the RFC basically just codifies what IE and IIS did in Windows 2000 and was later adopted by everyone else.
RFC 7235 does not mention SPNEGO, GSSAPI or Kerberos; it’s unrelated. Well it’s HTTP, obviously it is related, but not related to how this header works in this context.
That may be overstating things, but I’ll give it a shot.