I am trying to implement impersonation for a Blazor WebAssembly Solution. I chose to use the SignInManager because it seemed like the best (and only ?) solution for that. I found some documentation and applied their solution but the newly created cookie which should contains the info for the impersonated user still contains the info from the initial user even though it is a new cookie...
For further information, I use a customized Identity model (based on Microsoft guidelines) to match my database structure. The cookies are used with a MemoryCacheTicketStore to store user info in memory and not in the cookie (too much data to send them with every requests). The cookie only contains the user's email and an expiration date.
So I tried to implement the examples I found in a Controller:
[HttpPost]
[Authorize(Roles = "Data_User_Substitute")]
public async Task<IActionResult> Impersonate(string email)
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return Problem();
}
await _signInManager.SignOutAsync();
var userPrincipal = await _signInManager.CreateUserPrincipalAsync(user);
await this.HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, userPrincipal);
return Ok();
}
The code goes smoothly through it, in the HttpContext.Request.Cookies the old cookies is set to expire by _signInManager.SignOutAsync and a new one is created by this.HttpContext.SignInAsync.
But the next requests send by the application, when entering the RetrieveAsync Method from the TicketStore, use a key that still contains the info of the old user. And thus the old user is still authenticated.
public async Task<AuthenticationTicket?> RetrieveAsync(string key)
The thing is that in this Controller I also have an endpoint for Login only and one for Logout only et they work perfectly fine when called by the application. I think there must be something that is kept in memory and that is written in the cookie instead of the new user. But I could not find where.
What I expect is to have a new cookie containing the information about the impersonated user.
I found a roundabout solution that consists in using several requests to make the impersonation happen :
class ImpersonationModel
{
public string Email { get; set; }
public DateTimeOffset Timestamp { get; set; }
}
[HttpGet]
[Authorize(Roles = "Data_User_Substitute")]
public string ImpersonationToken([FromQuery] string email, [FromServices] CustomerAreaServerSettings settings)
{
using (var encryptor = SymmetricEncryptor.FromString("key")
{
var model = new ImpersonationModel { Email = email, Timestamp = DateTimeOffset.UtcNow };
string token = encryptor.Base64Encrypt(JsonSerializer.Serialize(model));
return token;
}
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Impersonate([FromQuery] string token, [FromServices] CustomerAreaServerSettings settings)
{
if (string.IsNullOrWhiteSpace(token)) throw new ArgumentException($"'{nameof(token)}' cannot be null or whitespace.", nameof(token));
string? email = null;
using (var encryptor = SymmetricEncryptor.FromString("key")
{
string json = encryptor.Base64Decrypt(token);
var model = JsonSerializer.Deserialize<ImpersonationModel>(json);
if (model is null) throw new ArgumentException("Invalid token");
if (model.Timestamp.AddMinutes(15) < DateTimeOffset.UtcNow) throw new Exception("Authentication token has expired");
email = model.Email;
}
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
return BadRequest("User does not exist");
await _signInManager.SignInAsync(user, false);
var newUser = this.HttpContext.User;
return Ok();
}