User impersonation in MVC using ASP.NET Identity 2

Last Updated: June 22, 2015 | Created: June 19, 2015
Quick Summary
This post is about how impersonate another user in a modern ASP.NET MVC web application that uses Identity 2. It describes a system that takes advantage of MVC 5’s new AuthenticationFilter to add or change the claims on a user so that they can ‘semi-impersonate’ another user without giving them too much access.

ASP.NET Identity 2 (referred to as Identity2 from now on) is a great system for handling site membership, i.e. who can log in, referred to as authentication, and what they are allowed to do, called authorisation (UK spelling, authorization in .NET code). Identity2 has a host of new features, such as allowing social logins, e.g. login with your Goggle or Facebook account, and it also works well with modern, scaled applications because it uses a cookie for holding session information.

The problem I had was that I wanted to allow a suitably authorised person to ‘impersonate’ another user, i.e. they could gain access to certain data that was normally read-only to anyone other than the author. Identity2 changes how you do that and I went hunt for solutions.

I found a great solution by Max Vasilyev, see his article User impersonation with ASP.Net Identity 2, which I nearly went with. However my need was slightly different to Max’s so I tackled it a different way, which I call ‘semi-Impersonation’ for reasons you will see later. Like Max having got something to work I thought I would also write a blog to share a solution.

My Use-Case

Before I describe the solution I wanted to say why I needed this impersonation method, then the code should make more sense.

I am working on a web application where graphic designers can set up designs for others to buy. They have their own ‘key’ which protects their designs from being changed by anyone other than them. The problem is, sometimes they need some help on setting up a design and its often easier for the site’s chief designer, called SuperDesigner from now on, to sort it out for them.

There are all sorts of ways for solving this from the downright dangerous (i.e. sharing passwords), to not that nice (i.e. allowing the SuperDesigner to have write access to all designs all of the time). In the end I decided to use a ‘semi-impersonate’ method, i.e. the SuperDesigner could just change their ‘key’ to the ‘key’ of the designer that needed help.

Technically the ‘key’ is a Claim, which is was Identity2 uses for authorisation.  If you are not familiar with claims they are a Key/Value pairs that each user has. Some are fixed and some you can add yourself.

My semi-impersonation solution

The solution uses a semi-impersonation approach, i.e. I changes a small part of the user information which allows access to the Designer’s work. The solution  consists of four main parts:

  1. A session cookie to control when we are in ‘impersonate’ mode.
  2. Use of MVC’s 5 new AuthenticationFilter OnAuthentication method which changes the claims on the current user.
  3. Some change to my application code that provides the ‘key’ for each designer.
  4. Feedback to the SuperDesigner that they are in ‘impersonate’ mode and a button to drop out of impersonation.

The big picture of the design is as follows:

  1. The SuperDesigner looks through a list of designers and clicks ‘Impersonate’ on the designer that needs help.
  2. That creates an Impersonate session cookie which holds the ‘key’ of that designer, encrypted of course!
  3. My Impersonate AuthenticationFilter is registered at startup and runs on every request. If it finds an Impersonate session cookie it adds a new, temporary Impersonate claim with the ‘key’ from the cookie to the current ClaimPrincipal.
  4. The code that provides the unique ‘key’ to each designer was changed to check for an Impersonate claim first, and if present it uses that key instead.
  5. The _LoginPartial.cshtml code is changed to visually show the SuperDesigner is in ‘impersonate’ mode and offers a ‘Revoke’ button, which deletes the cookie and hence returns to normal.

In MVC cookies are found my looking in HttpRequestBase Cookies collection, and added or deleted by adding the HttpResponseBase Cookies collection. I didn’t find the MSDN documentation on Cookies very useful, so here some code snippets to help. Note that in the code snippets the ‘_request’ variable holds the MVC Request property and ‘_response’ variable holds the MVC Response property.

public HttpCookie AddCookie(string value)
{
    var cookie = new HttpCookie(ImpersonationCookieName, Encrypt(value));
    _response.Cookies.Add(cookie);
    return cookie;
}

This code adds a Cookie to the HttpResponseBase cookie collection. A few things to note:

  • Because I don’t set a expiry date then it is a Session Cookie, i.e. it only lasts until the browser is closed, or until I set a negative Expiry date. That way it can’t hang around.
  • I encrypt the value using the MachineKey.Protect method to keep it safe.
  • I have a setting in my Web.Config to set all cookies to HTTPOnly, and Secure when running on Production systems.
public bool Exists()
{
    return _request.Cookies[ImpersonationCookieName] != null;
}

public string GetValue()
{
    var cookie = _request.Cookies[ImpersonationCookieName];
    return cookie == null ? null : Decrypt(cookie.Value);
}

Fairly obvious. Decrypt is the opposite of Encrypt

public void DeleteCookie()
{
    if (!Exists()) return;
    var cookie = new HttpCookie(ImpersonationCookieName) { Expires = DateTime.Now.AddYears(-1) };
    _response.Cookies.Add(cookie);
}

Deleting a cookie proved to be a bit troublesome and I found some misleading information on stackoverflow. The best article is the MSDN one here. Watch out that the delete cookies is new, don’t use the one from the request, and the new cookie must have exactly the same attributes as the original cookie. I had a .Path constraint on the added cookie and not on the delete cookie and it didn’t work.

2. AuthenticationFilter OnAuthentication

The AuthenticationFilter is a great addition that came in with MVC version 5. It allows the Current Principal, both Thread.CurrentPrincipal and HttpContext.User, to be changed. The key to doing this is the OnAuthentication method inside the AuthenticationFilter, which I have listed in full below:

/// <summary>
/// This looks for a Impersonation Cookie. If not found then it simply returns.
/// If found it changes the filterContext.Principal to include a
/// ImpersonateClaimType with the Designer Key value taken from the cookie.
/// </summary>
/// <param name="filterContext"></param>
public void OnAuthentication(AuthenticationContext filterContext)
{
    var impersonateCookie = new ImpersonationCookie(
        filterContext.HttpContext.Request,
        filterContext.HttpContext.Response);

    if (!impersonateCookie.Exists()) return;

    //There is an impersonate cookie, so we build a new
    //ClaimsPrincipal to replace the current Principal
    var existingCp = filterContext.Principal as ClaimsPrincipal;
    if (existingCp == null)
        throw new NullReferenceException("Should be claims principal");

    var claims = new List<Claim>();
    claims.AddRange(existingCp.Claims);
    claims.Add(new Claim(ImpersonateClaimType,
         impersonateCookie.GetValue()));

    filterContext.Principal = new ClaimsPrincipal(
         new ClaimsIdentity(claims, "ClaimAuthentication"));
}

Hopefully the code is fairly obvious. The main thing to know is if you change the filterContext.Principal then MVC will change the Thread.CurrPrincipal. too, so you change gets propagated throughout your application. So, by creating a new ClaimsPrincipal with all the original claims, plus the new ImpersonateClaimType then this can be picked up by calling Thread.CurrPrincipal by casting it to a ClaimsPrincipal.

Note that the ‘new ImpersonationCookie(…’ is just my wrapper round my Cookie code shown earlier.

3. Picking up the key

This bit of code is specific to my application, but worth showing for completeness:

public static string GetDesignerKey()
{
   var claimsPrincipal = Thread.CurrentPrincipal as ClaimsPrincipal;
   if (claimsPrincipal == null) return null;  //system failure

   var impersonateClaim = claimsPrincipal.Claims
       .SingleOrDefault(x => x.Type == ImpersonateClaimType);
   if (impersonateClaim != null) return impersonateClaim.Value;

   var designerKeyClaim = claimsPrincipal
       .Claims.SingleOrDefault(x => x.Type == DesignerKeyClaimType);
   if (designerKeyClaim != null) return designerKeyClaim.Value;

   return null;   //system failure
}

You may wonder why I did not just change the DesignerKeyClaimType? I could have, but then it would have been hard for other parts of the system to find out if we were in impersonate mode, as we will see in the next section.

4. Feedback to user and ‘Revoke impersonation’

The final part is the feedback to the user that they are in ‘impersonate’ mode and offer them a ‘Revoke impersonation’ button. This I did in a Html helper method I wrote which went into the ‘if (Request.IsAuthenticated)’ path of  _loginPartial.cshtml. This helper did the following:

  1. It looked for the ImpersonateClaimType. If not there then output normal html.
  2. If ImpersonateClaimType there then:
    1. Say we are in impersonation mode
    2. Offer a ‘Revoke’ button that calls an MVC action which deletes the Impersonation cookie.

Why/how is my implementation different to Max Vasilyev solution?

It is always good to compare solutions so you know why they are different. Here are my thoughts on that (Max, please do tell me anything else you would like added).

  • Max Vasilyev’s solution is great if you want to take over all the user’s roles and claims – you become the user in total. In my case I just wanted to change one attribute, the designer key, hence the name ‘semi-impersonate’.
  • If you just need to get impersonation going quickly then use Max’s solution – it just works. My method needs more design work as you need target specific attributes/features for it to work. Therefore it is only worth considering my method if you want more control on how the impersonation works, see the points below:
    • My method only adds/changes the specific claims that are needed, so limiting the power of the impersonator.
    • My method would would with different levels/types of impersonation by having different cookies. The OnAuthentication method could then obey different rules for each cookie type.
    • My method can use the cookie attributes like Domain and Path to limit what part of my site supports impersonation. In my application I use the .Path attribute to make impersonation only work on the Designer setup area, thus restricting the power of the SuperDesigner to say edit the designer’s details  or payments.

Note: Max makes a good point in his feedback that using .Path in that way ‘is not future- or refactoring- proof’, i.e. if the routing is changed by someone who does not know about the .PATH on the cookie then it could lead to errors that are hard to find at testing time. You may want to take his advice on that and use a more obvious authentication filter attribute that checks for the cookie.

Other, more minor differences are:

  • My method allows for audit trails to work properly as only the key is changed and the rest of the user identity is left alone. Also means the user who is impersonation, SuperDesigner in my case, keeps their enhanced Authorisation Roles.
  • My method will be very slightly slower than Max’s solution as it runs the OnAuthentication method on every HTTP request.
  • My method is limited to what information you can put in  a cookie (4093 bytes). Also because I encrypt my content it can take up a lot of room so you might only get four or five big strings in an encrypted cookie.

Conclusion

This article has described a way of one user gaining access to facilities of another user in a secure and controlled way. Hopefully I have given you enough code and links to see how it works. I have also pointed you to Max Vasilyev solution, which is very good but uses a different approach, so now you have two ways to impersonate another user.

Do have a look at both and take your choice based on what you need to do, but between Max’s solution and mine I hope they help you with your web application development.

Happy coding!