User impersonation allows you to impersonate another user of your system, usually done for support purposes. There's a lot of discussion on how to do impersonation and way more heated talks if you should do it. Depending on the nature of your service you might need this feature, if that's not the case simply don't impersonate your users, as that's just another door you have to look after.

Server code

We need to ensure that this part can be called only from the support users, so controller authorization attributes should filter on some role.

We will need some identifier, to know who we are impersonating, some secret parameter (optional) that we don't expose anywhere, say this value only stays in DB, and a URL to redirect into after impersonation is done.

[SecurityHeaders]
[Authorize(Roles = "support")]
public class SupportController : Controller
{
    // ... di services here

    public async Task<IActionResult> Impersonate(string id, string returnUrl)
    {
        var redirectAllowed = RedirectIsAllowed(returnUrl);

        if (!redirectAllowed)
            return NoContent();

        var dbUser = await _userManager.FindByIdAsync(id);
        if (dbUser == null)
            return NotFound();

        var userRoles = await _userManager.GetRolesAsync(dbUser);
        if (userRoles.Where(x => x.Equals("admin")).Any())
            return Forbid();

        AuthenticationProperties authProps = new AuthenticationProperties
        {
            IsPersistent = false,
            ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(10)),
            RedirectUri = returnUrl,
            AllowRefresh = false,
            IssuedUtc = DateTime.UtcNow
        };

        var idServerUser = new IdentityServerUser(dbUser.Id)
        {
            DisplayName = dbUser.UserName,
            AuthenticationTime = DateTime.UtcNow
        };

        await HttpContext.SignOutAsync();

        await HttpContext.SignInAsync(idServerUser, authProps);
        return Redirect(returnUrl);
    }

    private bool RedirectIsAllowed(string returnUrl)
    {
        // check if returnUrl is from domains you allow
    }
}

Mvc Client code

The client part is a bit tricky because we need to tell the aspnet core identity that the principal has been replaced. To archive that, let us return a Challenge result after impersonation, which will cause a redirect, then ProfileService will return the new user information.

 [Authorize(Roles = "support")]
	 public IActionResult Impersonate(Guid id)
	 {
	 if (id == Guid.Empty)
	 return BadRequest();
	 
	 var returnUrl = Url.ActionLink("Impersonated", "Account", new { challenge = true });
	 var impersonateUrl = $"https://<YOUR SERVER URL IN HERE>/Support/Impersonate/{id}/?returnUrl={returnUrl}";
	 return Redirect(impersonateUrl);
	 }

public IActionResult Impersonated(bool challenge)
{
    if (challenge)
    {
        return Challenge(new AuthenticationProperties()
        {
            RedirectUri = Url.Action("Index", "Home"),
            AllowRefresh = false
        }, "Cookies", "oidc");
    }

    return View();
}

Extras

Following Principle of least privilege, we can take several steps to improve this feature such as:

  • Add another parameter to the impersonation action, maybe some secret value stored in DB that you don't expose elsewhere
  • Allow only some small group of users to impersonate others
  • Deny admin/other-roles impersonation

Check This Github Gist for more complete code.

Enjoy :)