1

I have a regular ASP.Net Core web site that users access using Windows Authentication to determine which users can access which pages.

In order to render a page for the user, the site needs to call in to a series of web services to fetch various bits of data. These web services don't use Windows Authentication. Instead, they require the user's JWT Token.

So, our WebSite needs to exchange the user's Windows token for a JWT token. We have a special ExchangeToken web service that accepts a request using Windows Authentication, and returns the user's JWT Token.

The difficulty comes when I want WebSite to call this ExchangeToken web service. I need to call it using Impersonation, so that I get the user's JWT Token back. However, it doesn't appear to be possible to use HttpClient with Impersonation.

Initially, I had planned to do this in WebSite:

  1. Repeatedly...
  2. Impersonate the user
  3. Instantiate an HttpClient
  4. Call the TokenExchange service to get the JWT Token
  5. Dispose the HttpClient
  6. Stop impersonation
  7. Return the token

However, according to what I've read, re-creating an HTTP client for every call is bad practice, and I should be using HttpClientFactory instead.

However, I don't see how this approach can work with Impersonation.

I tried this:

  1. Use HttpClientFactory to create an HttpClient
  2. Repeatedly...
  3. Impersonate the user
  4. Call the TokenExchange service to get the JWT Token
  5. Stop impersonation
  6. Return the token

However, what happens is that, despite the impersonation, all calls to the TokenExchange service are made with the same windows credentials - the credentials of the user who happens to access the web site first. AFAIK, this stems from the way that Windows Authentication works - it performs a token exchange the first time you use an HttpClient, and from then on, all calls for that client use the same token.

One option would be to create a separate client for each user... but I have about 7,000 users, so that seems a bit excessive!

Another option would be to trust the WebSite to fetch the tokens on behalf of the user, using its own account. The problem with this is that it entails trusting the WebSite. If it is compromised by an attacker, then I can't stop the attacker stealing JWT tokens for arbitrary user. Whereas, with the impersonation, the attacker still can't get a user's JWT token without first obtaining their Windows token.

So, is there a way to do impersonation + IHttpClientFactory together? Or is there a better way to approach all this?

(If it matters, my company has its own Windows servers - we're not in the cloud, yet)

To demonstrate the problem with the second approach, I made a test application. It doesn't actually use HttpClientFactory, but it does demonstrate the problem.

I started with a web site that just returns the user who made a call:

[Authorize]
[Route("api/[controller]")]
[ApiController]
public class WhoController : ControllerBase
{
    [HttpGet]
    public ActionResult<string> Get()
    {
        return User.Identity.Name;
    }
}

My client code works like this:

    private void CallClient(HttpClient httpClient, string username, string password)
    {

        LogonUser(username, "MYDOMAIN", password, 2, 0, out IntPtr token);

        var accessTokenHandle = new SafeAccessTokenHandle(token);

        WindowsIdentity.RunImpersonated(
            accessTokenHandle,
            () =>
            {
                string result = httpClient.GetStringAsync("http://MyServer/api/who").Result;
                Console.WriteLine(result);
            });
    }

And my test code invokes it like this:

    public void Test()
    {
        var httpClient = new HttpClient(new HttpClientHandler { UseDefaultCredentials = true });
        CallClient(httpClient, "User1", "Password1");
        CallClient(httpClient, "User2", "Password2");

    }

As described above, I get the following written to the console:

User1
User1

What I want is:

User1
User2
Kramii
  • 8,379
  • 4
  • 32
  • 38
  • Can you share a little bit of code (or pseudocode) as to how the HttpClient is set up for negotiation of the credentials -> token and is persisting that authentication state? – Adam May 22 '20 at 13:22
  • I've added code to demonstrate that, after the first call, all subsequent calls to an httpClient use the same Windows credentials. – Kramii May 22 '20 at 13:40
  • Before I say a lot of words that may or may not be helpful, can you try an `httpClient.DefaultRequestHeaders.ConnectionClose = true;` before your `CallClient()` runs in your test? – Adam May 22 '20 at 20:41
  • With ConnectionClose = true I get an exception thrown, "Authentication failed because the connection could not be reused". It is thrown for User1, ie. the first call to CallClient. – Kramii May 23 '20 at 09:09

1 Answers1

0

TL;DR: NET Core is doing a lot to fight you on this approach under the hood.

Not entirely an answer on what to do, but hopefully helpful background on the HttpClientFactory approach, based on my understanding of the components.

First, from the ASP NET Core docs in regards to impersonation:

ASP.NET Core doesn't implement impersonation. Apps run with the app's identity for all requests, using app pool or process identity. If the app should perform an action on behalf of a user, use WindowsIdentity.RunImpersonated in a terminal inline middleware in Startup.Configure. Run a single action in this context and then close the context.

RunImpersonated doesn't support asynchronous operations and shouldn't be used for complex scenarios. For example, wrapping entire requests or middleware chains isn't supported or recommended.

As you call out, there's a lot of progress NET Core has made around how HttpClient instances are handled to resolve socket exhaustion and the expensive operations around the underlying handlers. First, there's HttpClientFactory, which in addition to supporting creating named/typed clients with their own pipelines, also attempts to manage and reuse a pool of primary handlers. Second, there's SocketsHttpHandler, which itself manages a connection pool and replaces the previous unmanaged handler by default and is actually used under the hood when you create a new HttpClientHandler. There's a really good post about this on Steve Gordon's Blog: HttpClient Connection Pooling in NET Core. As you're injecting instances of HttpClient around from the factory, it becomes way safer to treat them as scoped and dispose of them because the handlers are no longer your problem.

Unfortunately, all that pooling and async-friendly reuse makes your particular impersonation case difficult, because you actually need the opposite: synchronous calls that clean up after themselves and don't leave the connection open with the previous credentials. Additionally, what used to be a lower-level capability, HttpWebRequest now actually sits on top of HttpClient instead of the other way around, so you can't even skip it all that well by trying to run the requests as a one off. It might be a better option to look into using OpenID Connect and IdentityServer or something to centralize that identity management and Windows auth and pass around JWT everywhere instead.

If you really need to just "make it work", you might try at least adding some protections around the handler and its connection pooling when it comes to the instance that is getting used to make these requests; event if the new clients per request are working most of the time, deliberately cleaning up after them might be safer. Full disclaimer, I have not tested the below code, so consider it conceptual at best.

(Updated Switched the static/semaphore to a regular instance since the last attempt didn't work)

using (var handler = new SocketsHttpHandler() { Credentials = CredentialCache.DefaultCredentials, PooledConnectionLifetime = TimeSpan.Zero, MaxConnectionsPerServer = 1 })
using (var client = new HttpClient(handler, true))
    {
        return client.GetStringAsync(uri).Result;
    }
Adam
  • 3,339
  • 1
  • 10
  • 15
  • Interesting approach, but it doesn't work. It turns out that you can't change the credentials assigned to a SocketsHttpHandler after it has been used. If you try, you get a n InvalidOperationException, "This instance has already started one or more requests. Properties can only be modified before sending the first request.". – Kramii May 26 '20 at 08:56
  • 2
    Ahhh, sorry. Unfortunately, I think that's the beast that's going to bite you across most of the mechanisms here, as the pooling mechanisms are going to try their best to share that handler/connection. It may actually be your best option to construct a fresh client and handler per request and then ensure the handler and connections are disposed if this needs to work as-is. – Adam May 26 '20 at 13:01