ASP.NET Core 6.0 - Impersonate User

This article describes the implementation of user impersonation by administrators. I will assume you have downloaded the ASP.NET Core 6.0 - Users With Device 2FA Project.

Users With Device 2FA Project and Article Series

This article series is about the implementation of ASP.NET Core 6.0 with Visual Studio 2022. The ASP.NET Core 6.0 - Users With Device 2FA Project (UWD2FAP) implements WebAuthn, also known as FIDO2, instead of authenticator apps for two-factor authentication (2FA). The project implements Bootstrap v5 and Bootstrap Native. After a user registers, they can enable 2FA with Windows Hello, Android Lock Screen, or a FIDO2 security key. If I had this project when I created KenHaggerty. Com three years ago, I would have started with this user authentication template. The latest version is published at Preview. KenHaggerty. Com. I encourage you to register and evaluate multiple 2FA devices in Manage Account > Two-Factor Authentication. Details, screenshots, and related articles can be found at ASP.NET Core 6.0 - Users With Device 2FA Project. The details page includes the version change log.

User impersonation

There are a number of reasons why you might want to impersonate a user:

  • To help another user troubleshoot an issue. If your user roles are configured differently, it's possible that your UI will look different from theirs and you'll need to impersonate the other user to be able to see what they see.
  • You want to make changes on behalf of another user (for example, the other user is away on vacation and you want to manage their orders or run a report).
  • You're an administrator who's setting up user roles, and you want to preview what other users will be able to see depending on the permissions you grant them.
Google Ad Manager Help - User impersonation

While developing the claims-based authorization feature, I used two different browsers for testing the permissions and UI after granting a user an authorization claim. I would use Firefox to log in as an administrator to assign claims and would troubleshoot as the user on the development browser. I have used impersonation in other applications to support users. Integrating the claims-based authorization feature with the ASP.NET Core 6.0 - Users Without Passwords Project (UWPP6) and ASP.NET Core 6.0 - Users Without Identity Project (UWIP6) amplified the need for a user impersonation feature. Research on how to implement user impersonation with ASP.NET Core found only a few articles which described complex implementations.

The UWD2FAP, UWPP6, and UWIP6 implement a ImpersonateUserAsync method in the SignInService. The method evaluates and casts the current user's Identity as primaryIdentity, creates a new cookie with the primaryIdentity and AppSettings. ImpersonateUserScheme, creates a new claims principal for the impersonated AppUser with an ClaimTypes. AuthenticationMethod set to AppSettings. ImpersonateUserAuthentication, signs out the primaryIdentity, and signs in impersonated AppUser.

public async Task<SignInResult> ImpersonateUserAsync(AppUser appUser)
{
    if (Context.User.Identity is not ClaimsIdentity primaryIdentity)
        throw new InvalidOperationException();

    var authenticationProperties = new AuthenticationProperties();
    await Context.SignInAsync(AppSettings.ImpersonateUserScheme,
        new ClaimsPrincipal(primaryIdentity), authenticationProperties).ConfigureAwait(false);
    var userPrincipal = await CreateUserPrincipalAsync(appUser, authenticationProperties,
        AppSettings.ImpersonateUserAuthentication);
    await Context.SignOutAsync(AppSettings.ApplicationScheme).ConfigureAwait(false);
    await Context.SignOutAsync(AppSettings.TwoFactorUserIdScheme).ConfigureAwait(false);
    await Context.SignInAsync(AppSettings.ApplicationScheme, userPrincipal, authenticationProperties)
        .ConfigureAwait(false);
    return SignInResult.Success;
}

The ImpersonateUserScheme cookie is added to the AuthenticationBuilder in Program.cs.

builder.Services.AddAuthentication(AppSettings.ApplicationScheme)
...
    .AddCookie(AppSettings.ImpersonateUserScheme, options =>
    {
        options.Cookie.Name = AppSettings.ImpersonateUserScheme;
    });

The ClaimsIdentity class is a concrete implementation of a claims-based identity; that is, an identity described by a collection of claims. A claim is a statement about an entity made by an issuer that describes a property, right, or some other quality of that entity. Such an entity is said to be the subject of the claim. A claim is represented by the Claim class. The claims contained in a ClaimsIdentity describe the entity that the corresponding identity represents, and can be used to make authorization and authentication decisions. A claims-based access model has many advantages over more traditional access models that rely exclusively on roles. For example, claims can provide much richer information about the identity they represent and can be evaluated for authorization or authentication in a far more specific manner. ClaimsIdentity Class

The ImpersonateUserAsync method is called from the AppUser's authorization claims page in the Admin/ Authorization folder.

Adminstrator Can Impersonate All.

The impersonator must have the Super: Administrator or the Super: Security claim. A user cannot impersonate themselves.

Cannot Impersonate Self.

The Super: Security claim holder can impersonate all except administrators.

Security Cannot Impersonate Administrators.

Users without a Super: Administrator or the Super: Security claim cannot impersonate any.

Non-Super Users Cannot Impersonate Any.

The Impersonate User redirects to the Manage Account > Profile page impersonating the AppUser. The Manage Account link in the Bootstrap NavBar is highlighted and displays the AppUser's Login Name. The Exit Impersonation button is displayed when impersonating.

Impersonate User Redirects To User

The Exit Impersonation button calls the ExitImpersonateUserAsync method in SigInServices. The method retrieves the ImpersonateUserScheme cookie, evaluates and casts the cookie's Identity as primaryIdentity, rebuilds the AuthenticationProperties from the primaryIdentity's claims, creates a new claims principal for the primaryIdentity, signs out the impersonated AppUser, and signs in the new claims principal.

public async Task<SignInResult> ExitImpersonateUserAsync()
{
    var result = await Context.AuthenticateAsync(AppSettings.ImpersonateUserScheme).ConfigureAwait(false);
    await Context.SignOutAsync(AppSettings.ImpersonateUserScheme).ConfigureAwait(false);
    var primaryIdentity = result?.Principal?.Identities.First();
    if (primaryIdentity != null)
    {
        var authenticationProperties = new AuthenticationProperties
        {
            AllowRefresh = AppSettings.LoginRememberMeDays > 0,
            IsPersistent = bool.Parse(primaryIdentity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.IsPersistent &&
                c.Issuer == AppSettings.ClaimIssuer)?.Value ?? bool.FalseString)
        };
        var issuedClaim = primaryIdentity.Claims
            .FirstOrDefault(c => c.Type == ClaimTypes.AuthenticationInstant && c.Issuer == AppSettings.ClaimIssuer);
        if (issuedClaim != null && DateTimeOffset.TryParse(issuedClaim.Value, out var issuedUtc))
            authenticationProperties.IssuedUtc = issuedUtc;
        var expirationClaim = primaryIdentity.Claims
            .FirstOrDefault(c => c.Type == ClaimTypes.Expiration && c.Issuer == AppSettings.ClaimIssuer);
        if (expirationClaim != null && DateTimeOffset.TryParse(expirationClaim.Value, out var expiresUtc))
            authenticationProperties.ExpiresUtc = expiresUtc;

        var newPrincipal = new ClaimsPrincipal(new ClaimsIdentity(primaryIdentity.Claims, AppSettings.ApplicationScheme));
        await Context.SignOutAsync(AppSettings.ApplicationScheme).ConfigureAwait(false);
        await Context.SignOutAsync(AppSettings.TwoFactorUserIdScheme).ConfigureAwait(false);
        await Context.SignInAsync(AppSettings.ApplicationScheme, newPrincipal, authenticationProperties).ConfigureAwait(false);
        return SignInResult.Success;
    }
    return SignInResult.Failed;
}

The Exit Impersonation refreshes the Manage Account > Profile page.

public async Task<IActionResult> OnPostExitImpersonationAsync()
{
    if (await _signInService.ExitImpersonateUserAsync().ConfigureAwait(false) == SignInResult.Success)
    {
        StatusMessage = "Impersonation has ended. The permission claims have been refreshed.";
        return RedirectToPage();
    }
    else
    {
        StatusMessage = "Error: Exit Impersonation failed.";
        return RedirectToPage();
    }
}
Exit Impersonation Refreshes The Profile Page.

Notes

Remember Me

I updated the CreateUserPrincipalAsync method. I added IsPersistent, IssuedUtc, and ExpiresUtc claims to allow restoring the primaryIdentity's AuthenticationProperties when exiting impersonation.

/// <summary>
/// Creates a <see cref="ClaimsPrincipal"/> for the <paramref name="appUser"/>, asynchronous.
/// </summary>
/// <param name="appUser">The the user to create a <see cref="ClaimsPrincipal"/> for.</param>
/// <param name="authenticationProperties">Properties applied to the login and authentication cookie.</param>
/// <param name="authenticationMethod">Name of the method used to authenticate the user.</param>
/// <returns><see cref="Task{TResult}"/> for <see cref="ClaimsPrincipal"/></returns>
private async Task<ClaimsPrincipal> CreateUserPrincipalAsync(AppUser appUser,
    AuthenticationProperties authenticationProperties, string authenticationMethod)
{
    var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, appUser.Id.ToString(), ClaimValueTypes.Integer, AppSettings.ClaimIssuer),
            new Claim(ClaimTypes.Name, appUser.LoginName, ClaimValueTypes.String, AppSettings.ClaimIssuer),
            new Claim(ClaimTypes.Sid, appUser.SecurityStamp, ClaimValueTypes.String, AppSettings.ClaimIssuer),
            new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod, ClaimValueTypes.String, AppSettings.ClaimIssuer),
            new Claim(ClaimTypes.IsPersistent, authenticationProperties.IsPersistent.ToString(), ClaimValueTypes.Boolean, AppSettings.ClaimIssuer)
        };

    if (authenticationProperties.IssuedUtc != null)
        claims.Add(new Claim(ClaimTypes.AuthenticationInstant,
            authenticationProperties.IssuedUtc?.ToString() ?? string.Empty,
            ClaimValueTypes.DateTime, AppSettings.ClaimIssuer));

    if (authenticationProperties.ExpiresUtc != null)
        claims.Add(new Claim(ClaimTypes.Expiration,
            authenticationProperties.ExpiresUtc?.ToString() ?? string.Empty,
            ClaimValueTypes.DateTime, AppSettings.ClaimIssuer));

    var appUserClaims = await _userService.GetAppUserClaimsAsync(appUser.Id).ConfigureAwait(false);
    foreach (var appUserClaim in appUserClaims)
        claims.Add(new Claim(appUserClaim.Type, appUserClaim.Value, appUserClaim.ValueType!, AppSettings.ClaimIssuer));

    return new ClaimsPrincipal(new ClaimsIdentity(claims, AppSettings.ApplicationScheme));
}
Detect Impersonation

I create the impersonating claims principal with the AppSettings. ImpersonateUserAuthentication value for the AuthenticationMethod. This adds an AuthenticationMethod claim which can be used to detect impersonation. I tried to set the ClaimsIdentity's Label property to identify impersonation, but the Label's value is null after a page refresh. See ClaimsIdentity. Label not preserved in CookieAuth.

if (User.HasClaim(c => c.Type == ClaimTypes.AuthenticationMethod && c.Value == AppSettings.ImpersonateUserAuthentication &&
    c.Issuer == AppSettings.ClaimIssuer))
...
Disclosure

I added a new clause to the Sample Terms of Service page to disclose the use of impersonation.

  • 9.5 Administrators can impersonate registered users for testing and support purposes. The administrator has access to exactly what that user can access in the system, including the same menus, permissions, and data. Transactions and edits the administrator does while impersonating the user are recorded as having been done by that user.
Ken Haggerty
Created 02/01/22
Updated 03/11/22 19:01 GMT

Log In or Reset Quota to read more.

Article Tags:

Authorization Claims
Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?