UPD July 2017: If you are looking to do User Impersonation in Asp.Net Core, read this article: http://tech.trailmax.info/2017/07/user-impersonation-in-asp-net-core/

Recently I’ve migrated my project to ASP.NET Identity. One of the features I had in the project is “Impersonation”. Administrators could impersonate any other user in the system. This is a strange requirement, but business behind the project wanted it.

This is the old impersonation way:

  1. When admin wanted impersonation, system would serialise information about admin account (mostly username).
  2. Find account for impersonated user
  3. Create a new authentication cookie for impersonated user
  4. As data add serialised information about admin account to the cookie
  5. Set the cookie
  6. Redirect admin to client page.
  7. Bingo, admin logged in as a client user.

To de-impersonate repeate the process in reverse. Get data about admin from cookie data (if it is present), delete cookie for client-user, login admin user again. Bingo, admin is logged in as admin again.

Here is the article how the old way is implemented

This exact code did not work with Identity framework. I tried finding the solution online, but nothing was available. My question on Stackoverslow immediately got 4 up-votes, but no answers. So people are interested in doing it, but nobody published any material on this. So here I am -)

Claims

New Identity framework works with Claims. Claim is a bit of information (think string key-value pair) that is attached to a user. You can store a claim in database and it is restored every time when you get a user from a storage. Or you can assign claims to a user before signing them in. Later on you can easily check if a user has a claim. Think of this like checking a dictionary of strings if there is a required key with a required value, or extract a value from dictionary by a key. Probably this is clear as mud, but bear with me, I’ll get to a code and it’ll become all clear.

Impersonation

To get admin user logged in as somebody else – not a problem. Delete old auth-cookie, create a new one for another user, redirect. The problem is to detect that admin is impersonating. And then to de-impersonte the admin, but don’t allow other users to de-impersonate. So here I’m creating a new user identity and add extra claims to the user, saying impersonation is going on and who is impersonating. So here is the code:

public async Task ImpersonateUserAsync(string userName)
{
    var context = HttpContext.Current;

    var originalUsername = context.User.Identity.Name;

    var impersonatedUser = await userManager.FindByNameAsync(userName);

    var impersonatedIdentity = await userManager.CreateIdentityAsync(impersonatedUser, DefaultAuthenticationTypes.ApplicationCookie);
    impersonatedIdentity.AddClaim(new Claim("UserImpersonation", "true"));
    impersonatedIdentity.AddClaim(new Claim("OriginalUsername", originalUsername));

    var authenticationManager = context.GetOwinContext().Authentication;
    authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, impersonatedIdentity);
}

This is pretty standard way Identity framework is logging in users. Only here we add extra claims – one for “Yes, impersonation is happening” and another one for “Original username is admin”.

First is used to detect if we are impersonating, second is to de-impersonate the user back into admin rights.

To detect if impersonation happens here is an extension method:

public static bool IsImpersonating(this IPrincipal principal)
{
    if (principal == null)
    {
        return false;
    }

    var claimsPrincipal = principal as ClaimsPrincipal;
    if (claimsPrincipal == null)
    {
        return false;
    }


    return claimsPrincipal.HasClaim("UserImpersonation", "true");
}

And this would be used like this:

if(HttpContext.Current.User.IsImpersonating())
{
    // do my stuff for admins
}

To get original username, use this extension method:

public static String GetOriginalUsername(this IPrincipal principal)
{
    if (principal == null)
    {
        return String.Empty;
    }

    var claimsPrincipal = principal as ClaimsPrincipal;
    if (claimsPrincipal == null)
    {
        return String.Empty;
    }

    if (!claimsPrincipal.IsImpersonating())
    {
        return String.Empty;
    }

    var originalUsernameClaim = claimsPrincipal.Claims.SingleOrDefault(c => c.Type == "OriginalUsername");

    if (originalUsernameClaim == null)
    {
        return String.Empty;
    }

    return originalUsernameClaim.Value;
}

And you would call it like this:

HttpContext.Current.User.GetOrigianlUsername()

And this is how de-impersonation happens:

public async Task RevertImpersonationAsync()
{
    var context = HttpContext.Current;

    if (!HttpContext.Current.User.IsImpersonating())
    {
        throw new Exception("Unable to remove impersonation because there is no impersonation");
    }


    var originalUsername = HttpContext.Current.User.GetOriginalUsername();

    var originalUser = await userManager.FindByNameAsync(originalUsername);

    var impersonatedIdentity = await userManager.CreateIdentityAsync(originalUser, DefaultAuthenticationTypes.ApplicationCookie);
    var authenticationManager = context.GetOwinContext().Authentication;

    authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, impersonatedIdentity);
}

This is the basics of my process. I have a bit more checks and validations in place, mostly for null references. Also as a claim I store return url, where admin have started impersonation, so admin can be redirected to the original location where they started from. Also userManager is an instance of UserManager<User> that was injected by a constructor. I skipped that for brevity.

Update 29 February 2016

Quite a few readers here have complained that temporary claims are lost when the cookie is getting updated by SecurityStampValidator. I knew about this issue, but had no need to fix this as none of my clients have complained about it. Until last week when they mentioned this issue and got very confused when their impersonating session suddenly exploded.

The fix is as following: you need to replace SecurityStampValidator with your own to make sure claims with details about impersonation stay there after the cookie is updated. I’ve taken the original class code from Codeplex repository and modified to my needs:

public static class ImpersonatingSecurityStampValidator
{
    public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser>(
        TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentity)
        where TManager : UserManager<TUser, string>
        where TUser : class, IUser<string>
    {
        return OnValidateIdentity(validateInterval, regenerateIdentity, id => id.GetUserId());
    }

    public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser, TKey>(
        TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentityCallback,
        Func<ClaimsIdentity, TKey> getUserIdCallback)
        where TManager : UserManager<TUser, TKey>
        where TUser : class, IUser<TKey>
        where TKey : IEquatable<TKey>
    {
        if (getUserIdCallback == null)
        {
            throw new ArgumentNullException("getUserIdCallback");
        }
        return async context =>
        {
            var currentUtc = DateTimeOffset.UtcNow;
            if (context.Options != null && context.Options.SystemClock != null)
            {
                currentUtc = context.Options.SystemClock.UtcNow;
            }
            var issuedUtc = context.Properties.IssuedUtc;

            // Only validate if enough time has elapsed
            var validate = (issuedUtc == null);
            if (issuedUtc != null)
            {
                var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
                validate = timeElapsed > validateInterval;
            }
            if (validate)
            {
                var manager = context.OwinContext.GetUserManager<TManager>();
                var userId = getUserIdCallback(context.Identity);
                if (manager != null && userId != null)
                {
                    var user = await manager.FindByIdAsync(userId);
                    var reject = true;
                    // Refresh the identity if the stamp matches, otherwise reject
                    if (user != null && manager.SupportsUserSecurityStamp)
                    {
                        var securityStamp =
                            context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType);
                        if (securityStamp == await manager.GetSecurityStampAsync(userId))
                        {
                            reject = false;
                            // Regenerate fresh claims if possible and resign in
                            if (regenerateIdentityCallback != null)
                            {
                                var identity = await regenerateIdentityCallback.Invoke(manager, user);
                                if (identity != null)
                                {
                                    /**** CHANGES START HERE ****/
                                    if (context.Identity.FindFirstValue("UserImpersonation") == "true")
                                    {
                                        // need to preserve impersonation claims
                                        identity.AddClaim(new Claim("UserImpersonation", "true"));
                                        identity.AddClaim(context.Identity.FindFirst("OriginalUsername"));
                                    }
                                    /**** CHANGES END HERE ****/

                                    context.OwinContext.Authentication.SignIn(context.Properties, identity);
                                }
                            }
                        }
                    }
                    if (reject)
                    {
                        context.RejectIdentity();
                        context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
                    }
                }
            }
        };
    }
}

Most of this class stays as it is unchanged. I’ve inserted a few lines just before user is getting re-signed in – look for /**** CHANGES START HERE ****/. There I check if impersonation is in process. Then I re-add original username claim and “UserImpersonation” flag.

Once you have added this class to your solution, you need to go to App_Start/AuthConfig.cs file and change SecurityStampValidator to be ImpersonatingSecurityStampValidator. Something like this:

    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        // your other configuration. Be careful not to remove it
        Provider = new CookieAuthenticationProvider
        {
            OnValidateIdentity = ImpersonatingSecurityStampValidator.OnValidateIdentity<UserManager, ApplicationUser>(
                validateInterval: TimeSpan.FromMinutes(10),
                regenerateIdentity: (manager, user) => manager.CreateIdentityAsync(user))
        }
    });

This way you will get cookie invalidation if SecurityStamp is updated. And you will preserve your impersonation session until you cancel it.

  • Ian

    I am using Federated authentication, couldn’t get your code to work, any advise would be appreciated.

    • Never used Federated Authentication, so no help for you here.

  • Erez

    Thank you! This works perfectly…

  • Reuben Helms

    I’ve been looking for something similar. In my case, while doing a WebApi call that might connect as a webapi user used to represent an organisation, or client application (like WordPress), the I want the subsequent calls to impersonate a user after the user id in the message has been validated.

    At this point, I’m thinking that I will be doing a disposable object that will validate and impersonate for it’s life time, and then restore the original credentials when it is done, since I will want the token to be for the webapi user when it returns its content.

    My StackOverflow question is here :http://stackoverflow.com/questions/28871838/temporarily-changing-identity-with-webapi-2, and I will update when I have a working solution.

    • Code in you answer looks like it works for you. Though your impersonation happens within a single http call, where as I needed to extend this for a lot of requests. Glad this works for you.

      • Reuben Helms

        I’m glad you did the hard yards to get the ball rolling.

  • bethoDkns

    Thank u so much for this! works perfectly

  • Hi trailmax, really helpful article and I nearly went with your implementation, which is good. However my needs were slightly different so I ended up developed an approach which I call ‘semi-impersonation’ where I just change/add some claims to the current user to allow them to access some data.

    There isn’t anything much about this subject, so like you I wrote a blog post, see http://www.thereformedprogrammer.net/user-impersonation-in-mvc-using-asp-net-identity-2/. I took the liberty of comparing and contrasting my approach to yours as it helps people to see the differences when choosing an approach. Do have a look and tell me if I have missed anything. Always open to learning!

    • Jon, thanks for reaching out and adding to my work.

      I did look through your solution and spotted a minor problem – you use `Thread.CurrentPrincipal` in you `GetDesigner()` method. I would not do that. `Thread.CurrentPrincipal` is not always a logged in user, on startup phase of your application you’ll get Windows user that runs IIS and that can cause some very strange bugs that are not very reproducible. So I’d advise to replace with `HttpContext.Current.User` – under the hood it eventually reaches to `Thread.CurrentPrinciapal` but does a bit more checking for you and you won’t get the strange bugs, rather NullRef exceptions (which it should be, because no HTTP requests are available yet).

      Another thing I spotted was incorrectness in this sentence: “Because Max’s wants to totally impersonate the user he has to update the User database to add/remove claims.” My approach does not do any database changes, only changes the cookie when the impersonation starts. And if you blow away a cookie in the middle of the session without de-impersonating, nothing will go wrong for admin user.

      I only add claims to a already created identity (this line: `impersonatedIdentity.AddClaim(new Claim(“UserImpersonation”, “true”));`) and that is effectively adding claim data on a cookie before it is set – database is not affected by this.

      Also you depend on `Path` in a cookie to restrict the access. I’m not fond of this. It works, but it is not future- or refactoring- proof. If your application gets a change in routing, you’ll need to remember to update `Path` properties in the cookie setting routine. That’s not hard, only need to remember. But if somebody else is doing the change, most likely they will miss it and you’ll get a bug that is hard to spot on development stage. So instead of using `Path`, I’d add authentication attributes on controllers that should be accessible by “SuperDesigner” and these attributes would be checking for presence of your special cookie. But this is minor and totally not mandatory – I’m talking from a point of view of a QA Manager (that’s part of my job title currently) in an industrial-size (100+ controllers with 800+ views) application.

      Other than that – good solution! I’m happy that my work helped you to build the implementation for your requirements.

      • Hi Max,

        Thank you for some excellent feedback, and I have updated my article accordingly. I wondered if I could ask you some follow on questions?

        Firstly on the `Thread.CurrentPrincipal` not being valid. I wasn’t aware of that, but a bit of research pointed me to this article http://www.hanselman.com/blog/SystemThreadingThreadCurrentPrincipalVsSystemWebHttpContextCurrentUserOrWhyFormsAuthenticationCanBeSubtle.aspx . I assume this is the problem you are talking about.

        My thoughts are that because I am using MVC AuthenticationFilter, which is specifically designed for setting the CurrentPrincipal, then that would be pretty solid. Clearly I must not access ‘GetDesignerKey()’ outside of an HTTP request, but that is how the system works anyway. Am I missing anything here?

        On the cookie update I have removed my incorrect comment about updating the user database. My mistake is I didn’t read your code properly! I assumed because you were adding claims you had to update the database as my understanding is that you had to otherwise the claims would be lost when the authentication cookie expires (see http://stackoverflow.com/questions/23622047/identity-cookie-loses-custom-claim-information-after-a-period-of-time). How do you get round that, or is my understanding incorrect?

        I see your point about the use of .Path being a bit ‘hidden’ and could cause problems on refactoring. I have added a comment with a pointer to your suggestion. In our case the applciation isn’t that big (likely to be 30-40 controllers) and only two people working on the project so it will be fine. I like the fact that the feature is kept contained within the cookie code.

        • 1. Regarding `Thread.CurrentPrincipal` – yes, pretty much that problem. Though I have not seen this article, but came across the same problem myself. I have spent quite a lot of time resolving the problem – hence recommending my solution. However, there are references saying otherwise: http://stackoverflow.com/q/19683635/809357 So keep using it, but be aware of possible implications, but you should not come across them as you say you use it in a filter.

          2. Question about claims is valid – and the answer is just what I do (more or less). I have seen this problem and made sure on cookie re-creation impersonating claims are copied to a new cookie.

  • TwoPea A

    Firstly, this is a fantastic article and addresses just what I was grappling with. Thank YOU!

    I am a newbie to ASP.NET and Identity. Can you help clarify as to which class I should be adding the “extension” method snippets you have shared?

    Regards,
    2P

    • Extensions methods can go into any static class. I usually group my extensions into relevant groups and call the class name as `SomethingExtensions`. In this case I had `ClaimsPrincipalExtensions`. Name does not matter much, as you usually don’t type the class name for extension methods.

      • TwoPea A

        Thank you for the quick reply. The IsImpersonating() method is expecting a ClaimsPrincipal as a parameter. I’m trying to call this method from two places – once from inside a MVC controller and another from inside a view (.cshtml) . Would you happen to know how I can pass the ClaimsPrincipal from these two places? I tried searching for “getting ClaimsPrincipal” from inside MVC view but did not find anything helpful.

        Best Regards,
        2P

  • Matt Roberts

    Thanks for this post. I’ve currently got a solution based on this, and am working through some issues with it…

    I just wanted to re-iterate a point about cookie re-generation that Jon made. Your Impersonate code creates a new cookie (plus some claims) for the user you want to impersonate. So if you were to do anything to re-generate the cookie, then your code will effectively create a cookie for that impersonated user but without the claims stuff, so it will appear as though you’re logged on as that user.

    A big gotcha that would cause this is the default idenitty code that you get when you create a new mvc 5 project. It has code that re-generates the cookie every 30 mins:

    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity(
    validateInterval: TimeSpan.FromMinutes(30),
    regenerateIdentityCallback: (manager, user) =>
    {
    return user.GenerateUserIdentityAsync(manager);
    },
    getUserIdCallback: id => (int.Parse(id.GetUserId()))
    ),

    So when GenerateUserIdentityAsync is called, it’s going to create an identity for your impersonated user, and lose the claims about the impersonation.

    I’m just sharing this because it’s an issue I currently have. So far my workarounds have been pretty hacky (session data, or additional cookies like Jon used in his version).

    • That’s true, this code will throw away all your custom claims from the cookie. But nothing stops you modifying this part `user.GenerateUserIdentityAsync(manager)` to check if impersonation happens and re-add the claims again to the new identity

      My clients have not raised a concern for this, and this code have been in production for over a year now, so must have been a non-issue.

    • See update to the post if you still interested in a fix to your issue

  • Mathew

    thanks for the post, I am finding a minor bug, I always getting public static bool IsImpersonating(this IPrincipal principal) method returns true.

    can anybody help me on this? Thanks in advance.

    • Are you using modified version of my function? Can you step through it and check object `claimsPrincipal.Identity.Claims` – what claims do you see there?