3

I need to implement user impersonation through a HTTP header. For example, a user will send a request to /api/something with the header impersonate=someUser.

I tried to following process:

  1. User gets authenticated by one of multiple authentication schemes.
  2. The authenticated user gets replaced by the impersonated user, if it passes some security checks.
  3. The /api/something endpoint is called

I wrote some custom middleware for this, that runs just after the builtin authentication middelware:

if (!context.Request.Headers.TryGetValue("%Impersonation header%", out StringValues subject))
{
    await _next(context);
    return;
}

if (context.User?.Identity?.IsAuthenticated != true)
{
    // return error
}

...

context.User = impersonatedUser
await _next(context);

However, when it finally reaches the controller, the initial user is still used because the ClaimsPrincipal has been replaced by the default authorization into a new object with two identities. The first identity is the real user, the second identity is the impersonated user.

I could potentially resolve the user then using the second identity, but I'm not sure this process is following best practices?

Edit: this is for ASP.NET Core 2.2 / 3.1

Kevin
  • 2,258
  • 1
  • 32
  • 40
Aaron
  • 826
  • 10
  • 22

1 Answers1

0

configure IISServerOptions in Startup.cs

services.AddAuthentication(IISDefaults.AuthenticationScheme);
        services.AddSimpleRoleAuthorization<CustomWindowsAuthenticationProvider>();
        services.Configure<IISServerOptions>(opt=>{
            opt.AutomaticAuthentication=true;
            opt.AuthenticationDisplayName="SIMS";
        });

then implement your own IClaimsTransformation to validate the user and set the claims apropriately

public class CustomWindowsAuthenticationProvider : ISimpleRoleProvider
{
    public CustomWindowsAuthenticationProvider(UnitOfWork unitOfWork)
    {
        this._unitOfWork = unitOfWork;
    }
    private UnitOfWork _unitOfWork;

    public Task<ICollection<string>> GetUserRolesAsync(string userName)
    {
        ICollection<string> result = new string[0];
        string[] user = userName.Split("\\");
        var roles = _unitOfWork.UserMod.GetRolesForUser(user[1]);
        if (roles!=null)
            result = roles.Select(d => d.RoleName).ToArray();


        return Task.FromResult(result);
    }
}
public interface ISimpleRoleProvider
{
    #region Public Methods

    /// <summary>
    /// Loads and returns the role names for a given user name.
    /// </summary>
    /// <param name="userName">The login name of the user for which to return the roles.</param>
    /// <returns>
    /// A collection of <see cref="string" /> that describes the roles assigned to the user;
    /// An empty collection of no roles are assigned to the user.
    /// </returns>
    /// <remarks>
    ///     <para>Beware that this method is called for each controller call. It might impact performance.</para>
    ///     <para>
    ///     If Windows authentication is used, the passed <paramref name="userName" />
    ///     is the full user name including the domain or machine name (e.g "CostroDomain\JohnDoe" or
    ///     "JOHN-WORKSTATION\JohnDoe").
    ///     </para>
    ///     <para>
    ///     The returned roles names can be used to restrict access to controllers using the <see cref="AuthorizeAttribute" />
    ///     (<c>[Authorize(Roles="...")]</c>
    ///     </para>
    /// </remarks>
    Task<ICollection<string>> GetUserRolesAsync(string userName);

    #endregion
}
public class SimpleRoleAuthorizationTransform : IClaimsTransformation
{
    #region Private Fields

    private static readonly string RoleClaimType = ClaimTypes.Role;//  $"http://{typeof(SimpleRoleAuthorizationTransform).FullName.Replace('.', '/')}/role";
    private readonly ISimpleRoleProvider _roleProvider;

    #endregion

    #region Public Constructors

    public SimpleRoleAuthorizationTransform(ISimpleRoleProvider roleProvider)
    {
        _roleProvider = roleProvider ?? throw new ArgumentNullException(nameof(roleProvider));
    }

    #endregion

    #region Public Methods

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        // Cast the principal identity to a Claims identity to access claims etc...
        var oldIdentity = (ClaimsIdentity)principal.Identity;

        // "Clone" the old identity to avoid nasty side effects.
        // NB: We take a chance to replace the claim type used to define the roles with our own.
        var newIdentity = new ClaimsIdentity(
            oldIdentity.Claims,
            oldIdentity.AuthenticationType,
            oldIdentity.NameClaimType,
            RoleClaimType);

        // Fetch the roles for the user and add the claims of the correct type so that roles can be recognized.
        var roles = await _roleProvider.GetUserRolesAsync(newIdentity.Name);
        if(roles.Count>0)
            newIdentity.AddClaims(roles.Select(r => new Claim(RoleClaimType, r)));

        // Create and return a new claims principal
        return new ClaimsPrincipal(newIdentity);
    }

    #endregion
}
public static class SimpleRoleAuthorizationServiceCollectionExtensions
{
    #region Public Static Methods

    /// <summary>
    /// Activates simple role authorization for Windows authentication for the ASP.Net Core web site.
    /// </summary>
    /// <typeparam name="TRoleProvider">The <see cref="Type"/> of the <see cref="ISimpleRoleProvider"/> implementation that will provide user roles.</typeparam>
    /// <param name="services">The <see cref="IServiceCollection"/> onto which to register the services.</param>
    public static void AddSimpleRoleAuthorization<TRoleProvider>(this IServiceCollection services)
        where TRoleProvider : class, ISimpleRoleProvider
    {
        services.AddScoped<ISimpleRoleProvider, TRoleProvider>();
        services.AddScoped<IClaimsTransformation, SimpleRoleAuthorizationTransform>();
    }

    #endregion
}

after that you can host the app in iis, and use the iis authentication to determine what kind of method and settings you want to use.

gadasadox
  • 109
  • 1
  • 9
  • I'm not sure this is a valid solution. How is this supposed to be wired to an HTTP header? – Aaron Feb 03 '20 at 08:19
  • You are setting up Windows authentication, which is not related to my question. Also, I don't see any solution for wiring the impersonation header from the HTTP request. Using a claims transformer could be an interesting idea though. – Aaron Feb 03 '20 at 11:01