A screenshot of ASP.NET forms authenticated user impersonation in action: The user 'Bob' is currently impersonating the user 'Alice'.
Introduction
Part of building a complete web application involves building a decent administration and support interface. A very important part of a good support interface is the ability of support users to login as an arbitrary client user. At the company I work for, they have several people sitting next to their phone at the office using this functionality every day. The company offers some kind of software as a service, their (trusted) support employees handle support calls from client users who have trouble getting stuff done in the application. If the issue isn't resolved directly, the support users use the functionality described in this article to log in as the troubled user 'and be able to see what the actual user sees'. The support users then either talk the client through the procedure for whatever they want done, or do it themselves directly. Half of the time, the support users at the company I work for even get called and just asked to do something for the client in the client's account.
For such operations to run smooth, the support users need to be able to jump from the administration and support interface directly into the client accounts. We can't go about resetting password and stuff; when they get a client on the phone, they just should be able to do what they have to do as quickly and efficient as possible.
While logging an ASP.NET user in as another user is quite easy; in fact, a call to FormsAuthentication.SetAuthCookie()
will do the job; I, however, needed a way to really streamline this kind of functionality. I found it to be quite desirable for the support user to be able to jump right back to his or her support user account after impersonating the other user, without the hassle of having to really login as the support user again. Since I was unable to find any existing solutions, I developed the UserImpersonation
class and the LoginUserImpersonation
control presented in this article to tackle exactly that problem.
In this article, I will first briefly describe the requirements for using the UserImpersonation
class and how to use it. After that, I will go into more detail on how I implemented user impersonation, and the motivation behind the decisions I made during the development.
Note: This article is not about what is usually referred to as 'Windows user impersonation'. This article only deals with ASP.NET forms authenticated users.
Using the UserImpersonation class
Requirements
In order to be able to make use of the UserImpersonation
class, the following conditions and requirements apply:
- One must make use of cookie based FormsAuthentication in order to be able to make use of the
UserImpersonation
class.
- Cookie encryption is highly recommended while making use of the
UserImpersonation
class.
- Only tested on .NET 2.0. The
UserImpersonation
class should thus work on .NET 2.0, 3.0, and 3.5.
- You, at the moment, do not make use of the
FormsAuthenticationTicket.UserData
property in your ASP.NET application/website.
Reference
Using the UserImpersonation
class is petty straightforward. The UserImpersonation
is located in the System.Web.Security
namespace, and has the following members:
public static void ImpersonateUser(string userName);
public static void ImpersonateUser(string userName, string returnUrl);
public static void Deimpersonate();
private static string Serialize(string userName, string returnUrl);
private static bool Deserialize(string data, out string userName, out string returnUrl);
public static string PrevUserName { get; }
public static bool IsImpersonating { get; }
Quick start
In order to start using the user impersonation functionality right away in your web application, follow the steps below:
- Download the
FormsAuthenticationUserImpersonation
precompiled binary into the /Bin folder of your ASP.NET application/website. This operation is equal to 'adding a reference' to the FormsAuthenticationUserImpersonation
precompiled binary for your ASP.NET application/website.
- Register the
FormsAuthenticationUserImpersonation
assembly on the page you wish to use the LoginUserImpersonation
control. This will likely be your master page. Registering an assembly is done using the <%@ Page %>
directive as follows:
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="MasterPage.aspx.cs" Inherits="_Default"
Title="Your masterpage" %>
<%@ Register Assembly="FormsAuthenticationUserImpersonation"
Namespace="System.Web.UI.WebControls" TagPrefix="asp" %>
- Add the
LoginUserImpersonation
control at the desired location on your ASP.NET page. Typically, you'll want to place this next to the login/logout button. The LoginUserImpersonation
control will be rendered as a link button with the text 'Return to {UserName}':
<asp:LoginUserImpersonation ID="LoginUserImpersonation1" runat="server" />
- Add the following code to your code where you want to actually start the user impersonation, where
strUserName
contains the name of the user you want to be logged in as. It is advised to redirect the user right after user impersonation is started, in order to make sure the changes take effect:
UserImpersonation.ImpersonateUser(strUserName);
Response.Redirect("~/YourPage.aspx");
Included example and source code
The included example website uses a rudimentary read-only MembershipProvider providing two users: Bob and Alice. For both of the users, the password is test.
The example website features a home page with a button allowing the logged in user to impersonate Alice, a login page, and a page only accessible by Alice.
The UserImpersonation
class and LoginUserImpersonation
control were written in C#. The source code is documented, and should be fairly easy to understand for any programmer at least being lightly acquainted with ASP.NET and C#.
Development
I will now discuss some interesting points I came across while developing this solution.
Design goals
- Keep the user impersonation logic as close as possible to the existing user authentication logic.
- Allow the user to easily return to his or her original account after impersonating another user without the hassle of having to login onto his or her own account again.
Keeping track of the original user
The Problem
As I already mentioned in the introduction, having a user to login as another user is actually quite simple. Just logging in the support user as the troubled client user will, however, result in the support user having to login into his or her account after he or she wants to end impersonating the other user. While this is no big deal for some dude administrating some small web application once a month, this will get really annoying for people working at the office helping clients all day using the application I am developing at the moment.
In short, I needed some way to log an user in as another user while at the same time keep track of the user he or she was previously fully authenticated for. This all needed to be done in a secure manner, because I wanted to give the user the ability to return to his or her original user account with a single mouse click, no re-entering passwords involved.
Authentication cookies
One secure and obvious way to handle this problem would be to keep track of the previous user by storing this information is the Session
property bag. This, however, would require sessions to be enabled on every single page, or an HTTP handler reachable by authenticated users, and would require extra tracking code because the session does not necessarily expire when the user authentication cookie expires. I, therefore, decided it was not optimal to store the previous user information in the session.
The most logical place to store this information would be the authentication cookie itself, wouldn't it? It's securely encoded, and all information expires when the cookie expires (duh). The authentication cookie, however, is a little less accessible than the Session
property bag. After some sniffing around on MSDN, I noted the FormsAuthenticationTicket
class, which in a way represents the decoded version of the authentication cookie actually exposed as a property which allows one to include custom data with the cookie: FormsAuthenticationTicket.UserData
.
Piggyback on the authentication cookie
When stating user impersonation, I use the following code to create a new authentication ticket for the user to be impersonated, piggybacking the data which describes the previous user as shown in the following code, which is taken from the UserImpersionation.ImpersonateUser
function:
HttpContext context;
FormsAuthenticationTicket authTicket;
HttpCookie authCookie;
string strSerializedData;
context = HttpContext.Current;
strSerializedData = Serialize(context.User.Identity.Name, returnUrl);
authCookie = FormsAuthentication.GetAuthCookie(userName, false);
authTicket = FormsAuthentication.Decrypt(authCookie.Value);
authTicket = new FormsAuthenticationTicket(authTicket.Version, authTicket.Name,
authTicket.IssueDate, authTicket.Expiration,
authTicket.IsPersistent, strSerializedData, authTicket.CookiePath);
authCookie.Value = FormsAuthentication.Encrypt(authTicket);
context.Response.Cookies.Add(authCookie);
First, the user name of the current user and an optional return URL are joined using a simple serialization function. Next, a new authentication cookie is created for the user to be impersonated and stored in the authCookie
variable. Since the FormsAuthentication
class directly produces an encrypted authentication cookie, this cookie is then decoded to a FormsAuthenticationTicket
class stored in the authTicket
variable.
Because all properties of the FormsAuthenticationTicket
class are read-only, a new FormsAuthenticationTicket
class instance is created effectively, cloning the previously obtained FormsAuthenticationTicket
class instance, but also carrying our serialized data. This new FormsAuthenticationTicket
class instance is then encrypted into our new authentication cookie, which sequentially is added to the response headers.
Retrieving the previous user name
After our new impersonation enabled authentication cookie has been set, it's quite straightforward to obtain the previous user name as shown in the following code, taken from the UserImpersionation.PrevUserName.get
property:
HttpContext context;
FormsIdentity formsIdentity;
string strUserName, strReturnUrl;
context = HttpContext.Current;
formsIdentity = (FormsIdentity)context.User.Identity;
if (string.IsNullOrEmpty(formsIdentity.Ticket.UserData))
return string.Empty;
if (!Deserialize(formsIdentity.Ticket.UserData,
out strUserName, out strReturnUrl))
return string.Empty;
return strUserName;
First, the User.Identity
property which exposes an IIdentity
interface by default is casted to an instance of the FormsIdentity
class. Note that the User.Identity
property only contains a FormsIdentity
class instance when the form authentication is used. Since the FormsIdentity
class exposes the FormsAuthenticationTicket
directly, the previous user name is easily obtained by deserializing the contents of the UserData
property.
Redirecting the user on deimpersonation
As you may have noticed while reading the article, an optional redirect URL is stored in the authentication cookie. The UserImpersonation.Deimpersonate()
method redirects the user to this location after impersonation is reverted.
In the application I am developing at the moment and use this user impersonation, the support users can impersonate some client user by clicking a button placed on a page describing the details of the concerned client user. I want the whole user impersonation experience to be like starting a new session, and ending thes session cleanly by arriving at the point where the session was started.
Performance
I suspect performance to be hardly affected by using the UserImpersonation
class. Yes, the previous user and redirect URL are included in the authentication cookie, but I'm having a hard time believing those 100 extra bytes will kill your performance even the slightest bit.
When considering performance extremes: I suspect this solution to be way more efficient than using the session for storing the previous user name and redirect URL. Assuming your application is running in a web farm, fetching the session will likely require an extra roundtrip toward the database server, taking anywhere between 20 and 100 ms. While I haven't tested this, I expect decrypting the authentication cookie will not even take a single millisecond.
Conclusion
I think I succeeded quite well implementing ASP.NET user impersonation. It surprised me I was unable to find any other examples of the functionality described in this article, since it seems this kind of stuff is likely to be present in most enterprise level applications, even while this whole user impersonation trick was not the most difficult thing to implement.
In this way, I want to contribute something back to the whole Open Source community which basically helped me from learning to program in the first place to solve many problems I now encounter as a professional programmer. As always, feel free to comment, did I miss something, is stuff not working, got a better solution? Drop a comment!
Links and references
History
- 18/07/2009 - Version 1.0 - Initial release.