1
Vote

Issue with HttpClient.GetAsync when default credentials are used (Preview 5)

description

Hi All
I don’t know if this is an issue, but I am having authentication problems with HttpClient.GetAsync when I use DefaultNetworkCredentials to set the HttpClientHandler.Credentials property in Preview 5. I have a service router that I wrote using the preview 4. This router only receives messages and forwards those messages to the real services. I used the Begin… End… async pattern to handle the request in the router. This is how the method to process Get request looks using Preview 4.

[OperationContractAttribute(AsyncPattern = true)]
[WebGet(UriTemplate = "*")]
public IAsyncResult BeginGet(HttpRequestMessage requestMessage, AsyncCallback callback, object asyncState)
{
 using (HttpClient client = CreateHttpClient(requestMessage))
 {
        Task<HttpResponseMessage> response = client.GetAsync(GetRelativeUri());
        return new CompletedAsyncResult<HttpResponseMessage>(response.ContinueWith<HttpResponseMessage>(t => t.Result).Result);
  }
}

public HttpResponseMessage EndGet(IAsyncResult result)
{
  CompletedAsyncResult<HttpResponseMessage> r = result as CompletedAsyncResult<HttpResponseMessage>;
  return r.Data;
}

The method to create the HttpClient instance is the following:
protected HttpClient CreateHttpClient(HttpRequestMessage requestMessage)
    {
        HttpClient client = new HttpClient(GetBaseAddress());
        HttpClientChannel channel = new HttpClientChannel();
        channel.PreAuthenticate = true;
        channel.Credentials = CredentialCache.DefaultNetworkCredentials;
        client.Channel = channel;

        // pass Accept Headers
        foreach (var header in requestMessage.Headers.Accept)
            client.DefaultRequestHeaders.Accept.Add(header);

        // pass AcceptCharset Headers
        foreach (var header in requestMessage.Headers.AcceptCharset)
            client.DefaultRequestHeaders.AcceptCharset.Add(header);

        // pass AcceptEncoding Headers
        foreach (var header in requestMessage.Headers.AcceptEncoding)
            client.DefaultRequestHeaders.AcceptEncoding.Add(header);

        // pass AcceptLanguage Headers
        foreach (var header in requestMessage.Headers.AcceptLanguage)
            client.DefaultRequestHeaders.AcceptLanguage.Add(header);

        return client;
    }
This router is hosted in IIS 7.5 with Windows Authentication and Impersonation enabled and it is working fine, the real services are also hosted in IIS 7.5 with the same authentication configuration as the router.

I am trying to rewrite it using Web Api preview 5 but using the new approach to process the requests asynchronously.
   [WebGet(UriTemplate = "*")]
    public Task<HttpResponseMessage> GetAsync(HttpRequestMessage requestMessage)
    {
        try
        {
            HttpClient client = CreateHttpClient(requestMessage);

            Task<HttpResponseMessage> response = client.GetAsync(GetRelativeUri());
            return response.ContinueWith<HttpResponseMessage>((t) =>
            {
                return t.Result;
            });
        }
        catch (Exception ex)
        {
            LogService.Log(ex.Message);
            throw new WebFaultException<String>(ex.Message, System.Net.HttpStatusCode.InternalServerError);
        }
I have modified the method to create the HttpClient to use the HttpClientHandler in order to specify the use of default credentials.
    protected HttpClient CreateHttpClient(HttpRequestMessage requestMessage)
    {
        HttpClientHandler httpClientHandler = new HttpClientHandler();
        httpClientHandler.PreAuthenticate = true;
        httpClientHandler.UseDefaultCredentials = true;
        HttpClient client = new HttpClient(httpClientHandler);
        client.BaseAddress = new Uri(GetBaseAddress());

        // pass Accept Headers
        foreach (var header in requestMessage.Headers.Accept)
            client.DefaultRequestHeaders.Accept.Add(header);

        // pass AcceptCharset Headers
        foreach (var header in requestMessage.Headers.AcceptCharset)
            client.DefaultRequestHeaders.AcceptCharset.Add(header);

        // pass AcceptEncoding Headers
        foreach (var header in requestMessage.Headers.AcceptEncoding)
            client.DefaultRequestHeaders.AcceptEncoding.Add(header);

        // pass AcceptLanguage Headers
        foreach (var header in requestMessage.Headers.AcceptLanguage)
            client.DefaultRequestHeaders.AcceptLanguage.Add(header);

        return client;
    }
When I use this new router I got errors related to authentication. If I use the same method to create the HttpClient using default credentials but the request is forwarded in a synchronous way (HttpClient.Get) everything works fine, also if I use specific credentials:

HttpClientHandler.Credentials = new NetworkCredential(username, password, domain);

I can forward the request asynchronously and it works. So basically my conclusion is that async requests using default credentials are not working properly.

Any suggestions/solutions are very welcome. Thanks a lot in advance.

comments

app1918 wrote Oct 1, 2011 at 3:57 PM

Hi Guys

I compared the implementation of HttpClientChannel in preview 4 with the implementation of HttpClientHandler in preview 5, in the new HttpClientHandler there is this new piece of code.
            // BeginGetResponse/BeginGetRequestStream have a lot of setup work to do before becoming async
            // (proxy, dns, connection pooling, etc).  Run these on a separate thread.
            // Do not provide a cancellation token; if this helper task could be canceled before starting then 
            // nobody would complete the tcs.
            Task.Factory.StartNew(startRequest, state);
What is happening is that the StartRequest method is executed in another thread with the credentials of the asp.net process (application pool) not the credentials of the impersonated user. How can I force to use the impersonated user credentials in the thread where the StartRequest method is executed?

app1918 wrote Oct 1, 2011 at 4:02 PM

Sorry I forgot to mention that I have this in my web.config file

<runtime>
<legacyImpersonationPolicy enabled="false"/>
<alwaysFlowImpersonationPolicy enabled="true"/>
</runtime>

but when I check System.Security.SecurityContext.IsWindowsIdentityFlowSuppressed() inside StartRequest method it returns true.

app1918 wrote Oct 1, 2011 at 6:30 PM

Hi All
I think I found a solution. This is what I did.

In the class RequestState I put a new member to hold the current windows identity:
   internal System.Security.Principal.WindowsIdentity windowsIdentity;
In HttpClientHandler.SendAsync method I put a line of code to get the current WindowsIdentity and put it in the RequestState variable just before the line that creates the task.
    state.windowsIdentity = System.Security.Principal.WindowsIdentity.GetCurrent();
    Task.Factory.StartNew(startRequest, state);
Then in the StartRequest method I placed the following two lines of code before the try block
    if (state.webRequest.UseDefaultCredentials && state.windowsIdentity != null)
            state.windowsIdentity.Impersonate();
So, if UseDefaultCredentials property in the HttpWebRequest is true then I impersonate in the current thread the windows identity of the thread where the Task was created. Now my service router is working as expected.

Any comments/suggestions are more than welcome.

danroth27 wrote Oct 4, 2011 at 3:46 PM

Thank you for reporting this issue. We are now actively investigating and we will let you know when the issue has been resolved.

legos211 wrote Apr 25, 2012 at 3:08 AM

Hello, I think I am having an issue the same as this one or similar.

I have an asp.net mvc site running as a client to an asp.net mvc4 web api service. The client site is set to use identity impersonation and I have set the following httpclienthandler values:
        var clientHandler = new HttpClientHandler();
        clientHandler.UseDefaultCredentials = true;
        clientHandler.PreAuthenticate = true;
        clientHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
        var httpClient = new HttpClient(clientHandler);
        return httpClient;
The client site authenticates me but the httpclient does not seem to authenticate to the service at all. I can load the service using windows authentication by going directly to a service URL in my web browser. I get prompted for authentication and then get the requested data.

I can't seem to find what I am missing on this one.

Any updates to this thread?