How to get HttpClient to pass credentials along with the request?
Asked Answered
L

10

208

I have a web application (hosted in IIS) that talks to a Windows service. The Windows service is using the ASP.Net MVC Web API (self-hosted), and so can be communicated with over http using JSON. The web application is configured to do impersonation, the idea being that the user who makes the request to the web application should be the user that the web application uses to make the request to the service. The structure looks like this:

(The user highlighted in red is the user being referred to in the examples below.)


The web application makes requests to the Windows service using an HttpClient:

var httpClient = new HttpClient(new HttpClientHandler() 
                      {
                          UseDefaultCredentials = true
                      });
httpClient.GetStringAsync("http://localhost/some/endpoint/");

This makes the request to the Windows service, but does not pass the credentials over correctly (the service reports the user as IIS APPPOOL\ASP.NET 4.0). This is not what I want to happen.

If I change the above code to use a WebClient instead, the credentials of the user are passed correctly:

WebClient c = new WebClient
                   {
                       UseDefaultCredentials = true
                   };
c.DownloadStringAsync(new Uri("http://localhost/some/endpoint/"));

With the above code, the service reports the user as the user who made the request to the web application.

What am I doing wrong with the HttpClient implementation that is causing it to not pass the credentials correctly (or is it a bug with the HttpClient)?

The reason I want to use the HttpClient is that it has an async API that works well with Tasks, whereas the WebClient's asyc API needs to be handled with events.

Lord answered 31/8, 2012 at 9:2 Comment(8)
Possible duplicate of https://mcmap.net/q/129141/-unable-to-authenticate-to-asp-net-web-api-service-with-httpclient/1045728Symbolist
It seems that HttpClient and WebClient consider different things to be DefaultCredentials. Did you try HttpClient.setCredentials(...) ?Boarding
BTW, WebClient has DownloadStringTaskAsync in .Net 4.5, which can also be used with async/awaitPertinent
@L.B: we cannot upgrade to .Net 4.5 (yet), so for now I am stuck with the .Net 4.0 implementation.Lord
@GermannArlington: HttpClient doesn't have a SetCredentials() method. Can you point me to what you mean?Lord
HttpClientHandler does. blogs.msdn.com/b/henrikn/archive/2012/08/07/… - I actually meant to put HttpClientHandler.setCredentials(...) in the original comment but copied wrong class nameBoarding
@GermannArlington: Ah, ok. I cannot set the credentials explicitly using that call as it requires an ICredentials objects which I don't have as I'm using Windows Authentication.Lord
It would appear this has been fixed (.net 4.5.1)? I tried creating new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true } on a web server accessed by a Windows-authenticated user, and the web site did authenticate for another remote resource after that (would not authenticate without the flag set).Salvatore
W
76

I was also having this same problem. I developed a synchronous solution thanks to the research done by @tpeczek in the following SO article: Unable to authenticate to ASP.NET Web Api service with HttpClient

My solution uses a WebClient, which as you correctly noted passes the credentials without issue. The reason HttpClient doesn't work is because of Windows security disabling the ability to create new threads under an impersonated account (see SO article above.) HttpClient creates new threads via the Task Factory thus causing the error. WebClient on the other hand, runs synchronously on the same thread thereby bypassing the rule and forwarding its credentials.

Although the code works, the downside is that it will not work async.

var wi = (System.Security.Principal.WindowsIdentity)HttpContext.Current.User.Identity;

var wic = wi.Impersonate();
try
{
    var data = JsonConvert.SerializeObject(new
    {
        Property1 = 1,
        Property2 = "blah"
    });

    using (var client = new WebClient { UseDefaultCredentials = true })
    {
        client.Headers.Add(HttpRequestHeader.ContentType, "application/json; charset=utf-8");
        client.UploadData("http://url/api/controller", "POST", Encoding.UTF8.GetBytes(data));
    }
}
catch (Exception exc)
{
    // handle exception
}
finally
{
    wic.Undo();
}

Note: Requires NuGet package: Newtonsoft.Json, which is the same JSON serializer WebAPI uses.

Whoever answered 1/10, 2012 at 14:43 Comment(2)
I did something similar in the end, and it works really well. The asynchronous issue is not a problem, as I want the calls to block.Lord
WebClient is vastly different from HttpClient and ultimately WebClient is more limited (if you POST using UploadValues, for instance, you can't get a Stream response). The HttpClient based solution is better if you are already using or need to use HttpClient.Johnston
D
192

You can configure HttpClient to automatically pass credentials like this:

var myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
Duplessismornay answered 15/4, 2013 at 15:20 Comment(6)
I know how to do that. The behaviour is not what I want (as stated in the question) - "This makes the request to the Windows service, but does not pass the credentials over correctly (the service reports the user as IIS APPPOOL\ASP.NET 4.0). This is not what I want to happen."Lord
this seems to fix my issue where iis only has windows authentication enabled. if you just need some legit credentials passed, this should do it.Searching
Not sure this works the same as WebClient in impersonation/delegation scenarios. I get "The target principal name is incorrect" when using HttpClient with the above solution, but using WebClient with a similar setup passes the user's credentials through.Bentz
This did work for me and the logs show correct user. Although, with double hop in the picture, I did not expect it to work with NTLM as the underlying authentication scheme, but it works.Realist
Why does this work differently than System.Net.CredentialCache.DefaultCredentials or System.Net.CredentialCache.DefaultNetworkCredentials?Mirza
This technique worked perfectly for me. In my case, IIS iisSettings" had "windowsAuthentication": true, but the user credentials were not getting into the HttpClient.Forecourt
W
76

I was also having this same problem. I developed a synchronous solution thanks to the research done by @tpeczek in the following SO article: Unable to authenticate to ASP.NET Web Api service with HttpClient

My solution uses a WebClient, which as you correctly noted passes the credentials without issue. The reason HttpClient doesn't work is because of Windows security disabling the ability to create new threads under an impersonated account (see SO article above.) HttpClient creates new threads via the Task Factory thus causing the error. WebClient on the other hand, runs synchronously on the same thread thereby bypassing the rule and forwarding its credentials.

Although the code works, the downside is that it will not work async.

var wi = (System.Security.Principal.WindowsIdentity)HttpContext.Current.User.Identity;

var wic = wi.Impersonate();
try
{
    var data = JsonConvert.SerializeObject(new
    {
        Property1 = 1,
        Property2 = "blah"
    });

    using (var client = new WebClient { UseDefaultCredentials = true })
    {
        client.Headers.Add(HttpRequestHeader.ContentType, "application/json; charset=utf-8");
        client.UploadData("http://url/api/controller", "POST", Encoding.UTF8.GetBytes(data));
    }
}
catch (Exception exc)
{
    // handle exception
}
finally
{
    wic.Undo();
}

Note: Requires NuGet package: Newtonsoft.Json, which is the same JSON serializer WebAPI uses.

Whoever answered 1/10, 2012 at 14:43 Comment(2)
I did something similar in the end, and it works really well. The asynchronous issue is not a problem, as I want the calls to block.Lord
WebClient is vastly different from HttpClient and ultimately WebClient is more limited (if you POST using UploadValues, for instance, you can't get a Stream response). The HttpClient based solution is better if you are already using or need to use HttpClient.Johnston
N
32

What you are trying to do is get NTLM to forward the identity on to the next server, which it cannot do - it can only do impersonation which only gives you access to local resources. It won't let you cross a machine boundary. Kerberos authentication supports delegation (what you need) by using tickets, and the ticket can be forwarded on when all servers and applications in the chain are correctly configured and Kerberos is set up correctly on the domain. So, in short you need to switch from using NTLM to Kerberos.

For more on Windows Authentication options available to you and how they work start at: http://msdn.microsoft.com/en-us/library/ff647076.aspx

Nevels answered 31/8, 2012 at 20:29 Comment(12)
BlackSpy is right, you're basically describing a delegation scenario which is something the Windows Indentity Foundation handles as described in this articleUttica
"NTLM to forward the identity on to the next server, which it cannot do" - how come it does do this when using WebClient? This is the thing I don't understand - if it is not possible, how come it is doing it?Lord
When using web client it is still only one connection, between the client and the server. It can impersonate the user on that server (1 hop), but can't forward those credentials on to another machine (2 hops - client to server to 2nd server). For that you need delegation.Nevels
@BlackSpy: I don't understand your response. With WebClient, the credentials received by the web service are those of user X. When using the HttpClient, the credentials are the app pool of IIS. I want the WebClient behaviour but in the HttpClient.Lord
The only way to accomplish what you are trying to do in the manner you are trying to do it is to get the user to type his username and password into a custom dialog box on your ASP.NET application, store them as strings and then use them to set your identity when you connect to your Web API project. Otherwise you need to drop NTLM and move to Kerberos, so that you can pass the Kerboros ticket across to the Web API project. I highly recommend reading the link I attached in my original answer. What you are trying to do requires a strong understanding of windows authentication before you begin.Nevels
@BlackSpy: I have plenty of experience with Windows Authentication. What I am trying to understand is why the WebClient can pass on the NTLM credentials, but the HttpClient cannot. I can achieve this using ASP.Net impersonation alone, and not having to use Kerberos or to store usernames/passwords. This however only works with WebClient.Lord
It should be impossible to impersonate across more than 1 hop without passing the username and password around as text. it breaks the rules of Impersonation, and NTLM will not allow it. WebClient allows you to jump 1 hop because you pass up the credentials and run as that user on the box. If you look at the security logs you will see the login - the user logs into the system. You can't then run as that user from that machine unless you've passed the credentials as text and use another webclient instance to log onto the next box.Nevels
When you say "UseDefaultCredentials = true", NTLM is preventing you from passing the credentials on to the next server in the chain. Only Kerberos is allowed to do this.Nevels
I finally made it. This is two great articles that helped me : bugfree.dk/blog/2016/05/18/… blogs.msdn.microsoft.com/friis/2009/12/31/…Stink
I think the answer is that you are not actually using NTLM when credentials are being forwarded on to a second server whether you realize it or not. If it all of a sudden started working for you its because of Kerberos. Kerberos is now the default configuration for AD domains and has been available in ever evolving forms within AD since Server 2003. That being the case NTLM is also present on most implementations of AD as a fallback option should the Kerberos protocol fail to resolve a request. So before you continuing to troubleshoot this problem ensure Kerberos is configured correctly in AD!Bousquet
How do you force HttpClientHandler to use Kerberos and not NTLM?Apathetic
@Apathetic It's not a function of the HttpClient, it's configured in the operating system/domain, normally through Group Policy.Nevels
E
25

OK, so thanks to all of the contributors above. I am using .NET 4.6 and we also had the same issue. I spent time debugging System.Net.Http, specifically the HttpClientHandler, and found the following:

    if (ExecutionContext.IsFlowSuppressed())
    {
      IWebProxy webProxy = (IWebProxy) null;
      if (this.useProxy)
        webProxy = this.proxy ?? WebRequest.DefaultWebProxy;
      if (this.UseDefaultCredentials || this.Credentials != null || webProxy != null && webProxy.Credentials != null)
        this.SafeCaptureIdenity(state);
    }

So after assessing that the ExecutionContext.IsFlowSuppressed() might have been the culprit, I wrapped our Impersonation code as follows:

using (((WindowsIdentity)ExecutionContext.Current.Identity).Impersonate())
using (System.Threading.ExecutionContext.SuppressFlow())
{
    // HttpClient code goes here!
}

The code inside of SafeCaptureIdenity (not my spelling mistake), grabs WindowsIdentity.Current() which is our impersonated identity. This is being picked up because we are now suppressing flow. Because of the using/dispose this is reset after invocation.

It now seems to work for us, phew!

Exanimate answered 11/10, 2016 at 6:51 Comment(2)
Thank you so much for doing this analysis. This fixed my situation too. Now my Identity is passed across correctly to the other web application! You saved me hours of work! I'm surprised it isn't higher on the tick count.Igal
I only needed using (System.Threading.ExecutionContext.SuppressFlow()) and the issue was resolved for me!Demonize
S
15

In .NET Core, I managed to get a System.Net.Http.HttpClient with UseDefaultCredentials = true to pass through the authenticated user's Windows credentials to a back end service by using WindowsIdentity.RunImpersonated.

HttpClient client = new HttpClient(new HttpClientHandler { UseDefaultCredentials = true } );
HttpResponseMessage response = null;

if (identity is WindowsIdentity windowsIdentity)
{
    await WindowsIdentity.RunImpersonated(windowsIdentity.AccessToken, async () =>
    {
        var request = new HttpRequestMessage(HttpMethod.Get, url)
        response = await client.SendAsync(request);
    });
}
Stokowski answered 6/2, 2018 at 18:49 Comment(1)
Where did you get your identity from?Trixie
I
6

It worked for me after I set up a user with internet access in the Windows service.

In my code:

HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = System.Net.WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
.....
HttpClient httpClient = new HttpClient(handler)
.... 
Invade answered 1/11, 2017 at 8:45 Comment(0)
O
3

Ok so I took Joshoun code and made it generic. I am not sure if I should implement singleton pattern on SynchronousPost class. Maybe someone more knowledgeble can help.

Implementation

//I assume you have your own concrete type. In my case I have am using code first with a class called FileCategory
FileCategory x = new FileCategory { CategoryName = "Some Bs"};
SynchronousPost<FileCategory>test= new SynchronousPost<FileCategory>();
test.PostEntity(x, "/api/ApiFileCategories"); 

Generic Class here. You can pass any type

 public class SynchronousPost<T>where T :class
    {
        public SynchronousPost()
        {
            Client = new WebClient { UseDefaultCredentials = true };
        }

        public void PostEntity(T PostThis,string ApiControllerName)//The ApiController name should be "/api/MyName/"
        {
            //this just determines the root url. 
            Client.BaseAddress = string.Format(
         (
            System.Web.HttpContext.Current.Request.Url.Port != 80) ? "{0}://{1}:{2}" : "{0}://{1}",
            System.Web.HttpContext.Current.Request.Url.Scheme,
            System.Web.HttpContext.Current.Request.Url.Host,
            System.Web.HttpContext.Current.Request.Url.Port
           );
            Client.Headers.Add(HttpRequestHeader.ContentType, "application/json;charset=utf-8");
            Client.UploadData(
                                 ApiControllerName, "Post", 
                                 Encoding.UTF8.GetBytes
                                 (
                                    JsonConvert.SerializeObject(PostThis)
                                 )
                             );  
        }
        private WebClient Client  { get; set; }
    }

My Api classs looks like this, if you are curious

public class ApiFileCategoriesController : ApiBaseController
{
    public ApiFileCategoriesController(IMshIntranetUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }

    public IEnumerable<FileCategory> GetFiles()
    {
        return UnitOfWork.FileCategories.GetAll().OrderBy(x=>x.CategoryName);
    }
    public FileCategory GetFile(int id)
    {
        return UnitOfWork.FileCategories.GetById(id);
    }
    //Post api/ApileFileCategories

    public HttpResponseMessage Post(FileCategory fileCategory)
    {
        UnitOfWork.FileCategories.Add(fileCategory);
        UnitOfWork.Commit(); 
        return new HttpResponseMessage();
    }
}

I am using ninject, and repo pattern with unit of work. Anyways, the generic class above really helps.

Ormolu answered 27/11, 2012 at 21:54 Comment(0)
G
1

Set identity's impersonation to true and validateIntegratedModeConfiguration to false in web.config

<configuration>
  <system.web>
    <authentication mode="Windows" />
    <authorization>
      <deny users="?" />
    </authorization>
    <identity impersonate="true"/>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" ></validation>
  </system.webServer>
</configuration>
Gev answered 6/2, 2022 at 1:40 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Despond
F
0

Based on the solution from @Sean, I came up with this. With some mods this can also be used for GET and the returned string can be JsonSerializer.Deserialize(d) to an object or List of object.

public static string PostJsonString(string server, 
                                    string method, 
                                    HttpContent httpContent)
{
    string retval = string.Empty;
    string uri = server + method;

    try
    {
        // NOTE: the new HttpClientHandler() { UseDefaultCredentials = true } as the ctor
        //       parameter allows the existing user credentials to be used to make
        //       the HttpClient call.
        using (var httpClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true }))
        {
            using (var response = httpClient.PostAsync(uri, httpContent))
            {
                response.Wait();

                var result = response.Result;
                var readTask = result.Content.ReadAsStringAsync();
                readTask.Wait();

                retval = readTask.Result;

            }
        }

    }
    catch
    {
        throw;
    }

    return retval;
}
Forecourt answered 2/11, 2023 at 20:39 Comment(0)
F
-3
string url = "https://www..com";
System.Windows.Forms.WebBrowser webBrowser = new System.Windows.Forms.WebBrowser();
this.Controls.Add(webBrowser);

webBrowser.ScriptErrorsSuppressed = true;
webBrowser.Navigate(new Uri(url));

var webRequest = WebRequest.Create(url);
webRequest.Headers["Authorization"] = "Basic" + Convert.ToBase64String(Encoding.Default.GetBytes(Program.username + ";" + Program.password));
          
webRequest.Method = "POST";
        
Fernandez answered 26/10, 2022 at 9:25 Comment(2)
Hello, please see meta.stackoverflow.com/editing-help Thanks!Maximalist
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Despond

© 2022 - 2024 — McMap. All rights reserved.