Mixing Integrated Authentication and Anonymous Authentication with PreAuthenticated = true doesn’t work
This StackOverflow question indicate that it is half a bug and half a feature, but that it sure as hell looks like a bug to me.
Let us assume that we have a couple of endpoints in our application, called http://localhost:8080/secure and http://localhost:8080/public. As you can imagine, the secure endpoint is… well, secure, and requires authentication. The public endpoint does not.
We want to optimize the number of request we make, so we specify PreAuthenticated = true; And that is where all hell break lose.
The problem is that it appears that when using request with entity body (in other words, PUT / POST) with PreAuthenticate = true, the .NET framework will issue a PUT / POST request with empty body to the server. Presumably to get the 401 authentication information. At that point, if the endpoint that it happened to have reached is public, it will be accepted as a standard request, and processing will be tried. The problem here is that it has an empty body, so that has a very strong likelihood of failing.
This error cost me a day and a half or so. Here is the full repro:
static void Main() { new Thread(Server) { IsBackground = true }.Start(); Thread.Sleep(500); // let the server start bool secure = false; while (true) { secure = !secure; Console.Write("Sending: "); var str = new string('a', 621); var req = WebRequest.Create(secure ? "http://localhost:8080/secure" : "http://localhost:8080/public"); req.Method = "PUT"; var byteCount = Encoding.UTF8.GetByteCount(str); req.UseDefaultCredentials = true; req.Credentials = CredentialCache.DefaultCredentials; req.PreAuthenticate = true; req.ContentLength = byteCount; using(var stream = req.GetRequestStream()) { var bytes = Encoding.UTF8.GetBytes(str); stream.Write(bytes, 0, bytes.Length); stream.Flush(); } req.GetResponse().Close(); } }
And the server code:
public static void Server() { var listener = new HttpListener(); listener.Prefixes.Add("http://+:8080/"); listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication | AuthenticationSchemes.Anonymous; listener.AuthenticationSchemeSelectorDelegate = request => { return request.RawUrl.Contains("public") ? AuthenticationSchemes.Anonymous : AuthenticationSchemes.IntegratedWindowsAuthentication; }; listener.Start(); while (true) { var context = listener.GetContext(); Console.WriteLine(context.User != null ? context.User.Identity.Name : "Anonymous"); using(var reader = new StreamReader(context.Request.InputStream)) { var readToEnd = reader.ReadToEnd(); if(string.IsNullOrEmpty(readToEnd)) { Console.WriteLine("WTF?!"); Environment.Exit(1); } } context.Response.StatusCode = 200; context.Response.Close(); } }
If we remove pre authenticate is set to false, everything works, but then we have twice as many requests. The annoying thing is that if it would be trying to authenticate to a public endpoint, nothing would happen, if it were sending the bloody entity body along as well.
This is quite annoying.
Comments
Blog related bug report:
The previous link at the top of the page links to the current page, instead of to the "Stupid smart code: Solution" post I expected.
The blog posts "Stupid smart code: Solution" and "Stupid smart code" do a similar thing (link to themselves). Didn't check any further.
Just trying to complement your post:
1) HTTP already contains support for optimizing requests with body that may fail due to authentication requirements - see http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
2) In the above example, when accessing the public URI, the client assumes that NEGOTIATE authentication is required. Due to this, it immediately sends the first NEGOTIATE client message without a body, since another protocol round will be required. The reason for this assumption is described on the msdn docs http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.preauthenticate.aspx, namely "After a client request to a specific Uri is successfully authenticated, if PreAuthenticate is true and credentials are supplied, the Authorization header is sent with each request to any Uri that matches the specific Uri up to the last forward slash". If you change the URIs to "http://gaviao:8080/secure/" and "http://gaviao:8080/public/" (note the ending slash), the problems does not occur.
Pedro, 1) It doesn't send an Expect 2) It doesn't send the Authorization header. What it does is send a request expecting to get a 401 with the WWW-Authenticate details
It can not send the authorization header as the header will depend on the authentication method. And of course, until you get the 401 www-authenticate, you can not know what authentication method (the framework) will have to use.
The Expect should solve that, but since it is not the way the framework works...
Marcelo, Sure it know, it remembers that from the last time. If that has changed, it would get a 401 and recover
Nor sure if I follow you then: The FIRST time a request is, well... requested, the client can not know the authentication method. So it has to do TWO requests (empty one, with body the second). After that, all the requests are done as a single request.
That works for secure urls. Now, for public urls, since the system doesn't has a way to know if the url will be public or private in advance, it has to use the same algorithm. There is where things break. But you are telling the system to do so by instructing it setting the PreAuthenticate to true. Set the PreAuthenticate to "secure" and you should not have to have any problems.
Marcelo, a) on the first request, you don't know if the auth is there or not, so you submit with the request body. b) the actual problem is on the second request, not on the first one
Same issue with WCF Web API (expected) - http://wcf.codeplex.com/workitem/135
Also trying to solve this problem, thinking of adding something like Z-Authorizaion header that will do the job. However this is dirty solution.
Patrick, Yes, we had a bug with regards to time zone, that will be fixed now
Ayende, 1) Correct, it does not send the "Expect" header. However, IMHO, the HttpWebRequest could have use it to probe for authentication and continue with the body if the server responded with "100 Continue".
2) On my traces, the first request to the "public" URI (after a successful request to the "secure" URI) contains a "Authorization: NEGOTIATE xxx" header. The HttpWebRequest is doing this because it assumes the authentication method is the same as for the "secure" URI.
Mike,
Unfortunately, the Web API HttpClient uses HttpWebRequest underneath, so the behavior will be the same
After looking into it, I believe that the behavior is correct, and is an artifact of the integrated NTLM handshake.
read: http://www.innovation.ch/personal/ronald/ntlm.html
The empty requests, in their
Authorization
header, contain a "Type-1" message (see above article), which EXPECTS a response with a "Type-2" message, before the final request for the resource can be made with the result of the NTLM handshake, the "Type-3" message.If you point the sample application at IIS, after setting up a
public
virdir with onlyAnonymous Authentication
enabled and asecured
virdir with onlyWindows Authentication
enabled, you will see the NTLM handshake occur for thepublic
resource, even though anonymous access is allowed, because theAuthorization
header is present, with a "Type-1" message, i.e.:C -> S: PUT ... Authorization: Negotiate ... (Type-1) Content-Length: 0
S -> C: 401 Unauthorized Authorization: Negotiate ... (Type-2)
C -> S: PUT ... Authorization: Negotiate ... (Type-3) ... has request body
S -> C: 405 Method Not Allowed (Expected, as I did nothing but setup a virtual directory.)
So, as you can see, it still did the complete NTLM handshake, even though the request method is not allowed by the server.
Setting
PreAuthenticate
totrue
allows the client to skip the challenge part of HTTP authentication to determine which authentication protocol to use, but it does not allow the client to skip any handshaking that is built into the authentication protocol itself.Michael, The problem is, I don't see an Authorization header in the first request that fails.
Go to http://ayende.com/blog/postdetails/details. It throws an exception. Fix needed or disable link.
Comment preview