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>>();