0

I'm building an internal application that will use Web Api to communicate withfront-end applications and I'm also planning to use Windows Authentication.

Right now I'm using WebClient, and that is working fine with impersonation.

Has anyone been able to do this with HTTPClient without blocking and making asynchronous calls? It seems to me that HTTPClient is meant to be instantiated once for the application lifecycle and maybe that isn't conclusive to having each request having unique authentication via impersonation.

I've tried the code below, but I think the "GetStringAsync" method is what creates the new task, and I don't know how to get that to use the impersonated account instead of the app pool account.

WindowsIdentity impersonatedUser = WindowsIdentity.GetCurrent();
using (WindowsImpersonationContext ctx = impersonatedUser.Impersonate())
{
Test = RequestRouter.Client.GetStringAsync("ServiceURL").Result;
}
AnotherDeveloper
  • 1,242
  • 1
  • 15
  • 36
  • 2
    I'd avoid using raw HttpClient if I were you. Have a look into [Flurl.Http](http://tmenier.github.io/Flurl/fluent-http/) or [RestSharp](http://restsharp.org/). They avoid [some of the dangers of using HttpClient wrong](https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/) and provide a much cleaner abstraction. – mason Mar 02 '18 at 17:34
  • @mason Thank you. I'll give RestSharp a try. – AnotherDeveloper Mar 02 '18 at 18:10
  • 1
    I prefer Flurl, but to each their own. – mason Mar 02 '18 at 18:13
  • @mason I'll try both! – AnotherDeveloper Mar 02 '18 at 18:15
  • I don't think they're going to resolve your authentication issues. I don't have much experience with impersonation, but whatever problems that HttpClient has, likely Flurl and RestSharp will have the same issues. Are you sure that you need to authenticate as the user to your services? Why not just make sure that the web application itself has permission to access the service, and have the web application pass the user's username as part of the request (either in the header or the body)? This would get around your whole impersonation problem. – mason Mar 02 '18 at 18:19
  • @mason We have to authenticate at the front-end, but we also want to pass the identity to the service for the user's roles for authorization purposes. I can use WebClient since and instantiate it for every request, or at least change it's authentication credentials every request. The drawback there would be having to manage the tasks myself, and maybe lose some efficiencies that HTTPClient would have over WebClient. – AnotherDeveloper Mar 02 '18 at 18:30
  • 1
    I think you missed the point of what I'm saying. The service itself isn't going to be hit directly by an end user (right?) The web application is what the user is going to authenticate to. So have the web application pass the user's username to the service, and the service can use that information for authorization purposes. This completely cuts out the need to actually do impersonation. It changes it to a model where your web application makes requests on behalf of a particular user, rather than as a particular user. – mason Mar 02 '18 at 18:34
  • Yeah Identityserver4 oidc2 is good for authenticating client applications and integrates nice with Identity it alows you to create authorization by roles or policy. Only issue is I would never use it for unless im running asp.netcore1.x or lower. Had issues with 2.0core. – Mohamoud Mohamed Mar 02 '18 at 18:37
  • @mason. I see what you are saying. – AnotherDeveloper Mar 02 '18 at 18:51

1 Answers1

1

We had a similar need where we have a a web application that users interact with, and that web application makes Web API calls to a service on behalf of the user. Rather than trying impersonation (which is very tricky) we got around it by passing the username from the web application to the service via an HTTP header. The service checks for the header - if it doesn't see the header it throws an error. And we have some logic injectable to our controllers to grab the username from the header when needed.

We used ASP.NET Core for our service, but the same idea should work for ASP.NET as well.

Here's the Middleware we use to ensure the header is present and if it is, to grab it and store it:

public class RequireUsernameHeaderMiddleware
{
    private const string HEADER_NAME = "hps-username";
    private const string API_ROOT = "/api";
    private readonly RequestDelegate _next;

    public RequireUsernameHeaderMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IUsernameSetter usernameSetter)
    {
        // We only want to require the header on our API, not on our Swagger pages
        var pathString = new PathString(API_ROOT);

        if (context.Request.Path.StartsWithSegments(pathString))
        {
            if (!context.Request.Headers.Keys.Contains(HEADER_NAME))
            {
                context.Response.StatusCode = StatusCodes.Status400BadRequest;
                await context.Response.WriteAsync("Username is missing");
                return;
            }

            usernameSetter.Username = context.Request.Headers[HEADER_NAME];
        }

        await _next.Invoke(context);
    }
}

And then the glue:

public interface IUsernameSetter
{
    string Username { set; }
}

public class UsernameProvider : IUsernameProvider, IUsernameSetter
{
    public string Username { get; set; } = String.Empty;
}

//when configuring services in Startup
services.AddScoped<UsernameProvider>();
services.AddScoped<IUsernameProvider>(service => service.GetService<UsernameProvider>());
services.AddScoped<IUsernameSetter>(service => service.GetService<UsernameProvider>());

//This gets called before app.UseMvc() in Startup when configuring the IApplicationBuilder
app.UseMiddleware<RequireUsernameHeaderMiddleware>();

Now in our controllers:

readonly IUsernameProvider _usernameProvider;

public ProjectController(IUsernameProvider usernameProvider)
{
    _usernameProvider = usernameProvider;
}

public IActionResult SomeAction()
{
    _usernameProvider.Username; //now we can grab the username!
}

Not only does this remove the need for impersonation, it also simplifies testing of the controller.

Here's the Flurl code we use in our web application:

return await _apiOptions.Url.AppendPathSegment("Project")
                            .WithHeader("hps-username", username)
                            .GetJsonAsync<List<Project>>();
mason
  • 31,774
  • 10
  • 77
  • 121
  • Thanks @Mason. This is a good answer, it'll definitely inform my approach going forward. My hope was to have a custom role-provider handle this at every level as a cross-cutting solution for authentication-authorization, but it looks like I might need to abandon that as you have already been down that road. – AnotherDeveloper Mar 02 '18 at 18:59
  • 1
    What's stopping someone setting the header to any username they like and calling your service? – 4imble Nov 18 '20 at 09:03
  • @4imble I think you missed the conversation that took place in the comments on the question. In this scenario, a trusted service (ex: a service that you control) is making requests to the back-end service on the user's behalf. Obviously the only reason we can rely on the username in the header is that we trust the service calling it. If you don't have that trust level, then you likely have a publicly exposed API, and then the client should authenticate and authorize as themselves via normal means, without implementing this header mechanism. – mason Nov 18 '20 at 13:44