Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# WebClient NTLM authentication starting for each request

Tags:

Consider a simple C# NET Framework 4.0 application, that:

  • uses WebClient
  • authenticates using NTLM (tested on IIS 6.0 and IIS 7.5 server)
  • retrieves a string from an URL multiple times using DownloadString()

Here's a sample that works fine:

using System;
using System.Net;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string URL_status = "http://localhost/status";

            CredentialCache myCache = new CredentialCache();
            myCache.Add(new Uri(URL_status), "NTLM", new NetworkCredential("username", "password", "domain"));

            WebClient WebClient = new WebClient();
            WebClient.Credentials = myCache;

            for (int i = 1; i <= 5; i++)
            {
                string Result = WebClient.DownloadString(new Uri(URL_status));
                Console.WriteLine("Try " + i.ToString() + ": " + Result);
            }

            Console.Write("Done");
            Console.ReadKey();
        }
    }
}

The problem:

When enabling tracing I see that the NTLM authentication does not persist.

Each time Webclient.DownloadString is called, NTLM authentication starts (server returns "WWW-Authenticate: NTLM" header and the whole authenticate/authorize process repeats; there is no "Connection: close" header).

Wasn't NTLM supposed to authenticate a connection, not a request?

Is there a way to make WebClient reuse an existing connection to avoid having to re-authenticate each request?

like image 746
c4n Avatar asked Sep 30 '16 08:09

c4n


2 Answers

After 10 days of trying everything I could think of and learning a lot in the process, I finally figured a fix for this issue.

The trick is to enable UnsafeAuthenticatedConnectionSharing by overriding GetWebRequest and setting the property to true in the HttpWebRequest you get back.

You may want to combine that with the ConnectionGroupName property to avoid the potential use of the connection by unauthenticated applications.

Here is the sample from the question modified to work as expected. It opens a single NTLM authenticated connection and reuses it:

using System;
using System.Net;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string URL_status = "http://localhost/status";

            CredentialCache myCache = new CredentialCache();
            myCache.Add(new Uri(URL_status), "NTLM", new NetworkCredential("username", "password", "domain"));

            MyWebClient webClient = new MyWebClient();
            webClient.Credentials = myCache;

            for (int i = 1; i <= 5; i++)
            {
                string result = webClient.DownloadString(new Uri(URL_status));
                Console.WriteLine("Try {0}: {1}", i, result);
            }

            Console.Write("Done");
            Console.ReadKey();
        }
    }

    public class MyWebClient : WebClient
    {
        protected override WebRequest GetWebRequest(Uri address)
        {
            WebRequest request = base.GetWebRequest(address);

            if (request is HttpWebRequest) 
            {
                var myWebRequest = request as HttpWebRequest;
                myWebRequest.UnsafeAuthenticatedConnectionSharing = true;
                myWebRequest.KeepAlive = true;
            }

            return request;
        }
    }   
}

At this point I would also like to thank @Falco Alexander for all the help; while his suggestions didn't quite work for me, he did point me in the right direction to look for and finally find the answer.

like image 117
c4n Avatar answered Sep 26 '22 21:09

c4n


Check your IIS's setting, though that should be default.

<windowsAuthentication
   enabled="True"
   authPersistSingleRequest="False"
   UseKernelMode>

</windowsAuthentication>  

Ref: https://msdn.microsoft.com/en-us/library/aa347472.aspx

Did you check the zone your localhost IIS is in? This was also a pitfall from the client side in the past when working with WinInet. Check that default behaviour of WebClient.


Edit:

After reproducing the error, I could figure out it's the missing NTLM preauthentication implementation of WebClient that keeps you from a single 401 request:

var WebClient = new PreAuthWebClient();
WebClient.Credentials = new NetworkCredential("user", "pass","domain");

//Do your GETs 

Public class PreAuthWebClient: WebClient
{
    protected override WebRequest GetWebRequest (Uri address)
    {
        WebRequest request = (WebRequest) base.GetWebRequest (address);
        request.PreAuthenticate = true;
        return request;
  }
}
like image 44
Falco Alexander Avatar answered Sep 25 '22 21:09

Falco Alexander