Ayende @ Rahien

It's a girl

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.

Tags:

Posted By: Ayende Rahien

Published at

Originally posted at

Comments

Patrick Huizinga
12/22/2011 10:34 AM by
Patrick Huizinga

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.

Pedro Félix
12/22/2011 11:40 AM by
Pedro Félix

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.

Ayende Rahien
12/22/2011 01:01 PM by
Ayende Rahien

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

Marcelo Volmaro
12/22/2011 01:33 PM by
Marcelo Volmaro

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...

Ayende Rahien
12/22/2011 01:36 PM by
Ayende Rahien

Marcelo, Sure it know, it remembers that from the last time. If that has changed, it would get a 401 and recover

Marcelo Volmaro
12/22/2011 01:47 PM by
Marcelo Volmaro

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.

Ayende Rahien
12/22/2011 02:08 PM by
Ayende Rahien

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

Mike Chaliy
12/22/2011 02:36 PM by
Mike Chaliy

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.

Ayende Rahien
12/22/2011 02:50 PM by
Ayende Rahien

Patrick, Yes, we had a bug with regards to time zone, that will be fixed now

Pedro Félix
12/22/2011 03:56 PM by
Pedro Félix

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

Michael Morton
12/22/2011 09:51 PM by
Michael Morton

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 only Anonymous Authentication enabled and a secured virdir with only Windows Authentication enabled, you will see the NTLM handshake occur for the public resource, even though anonymous access is allowed, because the Authorization 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 to true 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.

Ayende Rahien
12/23/2011 10:02 AM by
Ayende Rahien

Michael, The problem is, I don't see an Authorization header in the first request that fails.

tester
12/24/2011 09:15 AM by
tester

Go to http://ayende.com/blog/postdetails/details. It throws an exception. Fix needed or disable link.

Comments have been closed on this topic.