Also available at

Also available at my website http://tosh.me/ and on Twitter @toshafanasiev

Wednesday, 9 March 2011

User Impersonation Class

I recently needed to impersonate a different user temporarily in an application. I've wrapped up the logic in the class below, I'll walk through the various features afterwards.

using System;
using System.Security.Principal;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.ComponentModel;
using System.Security;

public sealed class UserImpersonator
  : IDisposable
{
  #region fields

  private readonly WindowsImpersonationContext _context;

  private const int LOGON32_LOGON_INTERACTIVE = 2;
  private const int LOGON32_PROVIDER_DEFAULT = 0;

  #endregion

  #region constructor

  [PermissionSet(SecurityAction.Demand, Name="FullTrust")]
  private UserImpersonator(string user, SecureString password, string domain = null)
  {
    if (String.IsNullOrEmpty(user))
      throw new ArgumentException("user cannot be null or empty");

    if (String.IsNullOrEmpty(domain))
      domain = Environment.MachineName;

    if (password == null)
      throw new ArgumentNullException("password");

    IntPtr token = IntPtr.Zero;
    bool loginResult;

    {
      IntPtr pwd = IntPtr.Zero;
      try
      {
        pwd = Marshal.SecureStringToGlobalAllocUnicode(password);

        loginResult = LogonUser(
          user,
          domain,
          pwd,
          LOGON32_LOGON_INTERACTIVE,
          LOGON32_PROVIDER_DEFAULT,
          ref token
        );
      }
      finally
      {
        Marshal.ZeroFreeGlobalAllocUnicode(pwd);
      }
    }

    if (!loginResult)
    {
      int err = Marshal.GetLastWin32Error();
      throw new Win32Exception(err);
    }

    try
    {
      var identity = new WindowsIdentity(token);
      _context = identity.Impersonate();
    }
    finally
    {
      CloseHandle(token);
    }
  }

  #endregion

  #region methods

  public void Dispose()
  {
    _context.Undo();
  }

  public void EndImpersonation()
  {
    Dispose();
  }

  public static UserImpersonator Impersonate(string user, SecureString password, string domain)
  {
    return new UserImpersonator(user, password, domain);
  }

  [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
  private static extern bool LogonUser(
    string user,
    string domain,
    IntPtr password,
    int logonType,
    int logonProvider,
    ref IntPtr token
  );

  [DllImport("kernel32.dll")]
  private static extern bool CloseHandle(IntPtr handle);

  #endregion
}

I have made the constructor private, forcing the use of the factory method Impersonate and hence making it explicit that code running immediately after this call is running under the context of the user being impersonated; also I have implemented IDisposable to allow calling code to employ the using construct, as follows:

string user, domain;
SecureString password;

// initialise credentials

using ( var u = UserImpersonator.Impersonate( user, password, domain ) )
{
  // run code as different user
}

// back to previous user

I have also defined a more semantic EndImpersonation method that simply calls Dispose.
The implementation is quite simple - the credentials passed in are used to obtain an authentication token from Windows (via the LogonUser function exported by advapi32.dll) which is then used to create a System.Security.Principal.WindowsIdentity object which provides the impersonation context. Impersonation continues until Undo is called on the WindowsImpersonationContext object returned by WindowsIdentity.Impersonate - this is done in the Dispose method, linking UserImpersonator disposal to impersonation lifetime. Note: there was no need to implement a finalizer here as when the WindowsImpersonationContext is up for Garbage Collection, it's own finalizer will take care of business.
There are a couple of implementation details that are worth noting:
My declaration of LogonUser includes strings for the username and domain but a pointer (IntPtr) for the password. The reason for this is that I don't want the user's password sitting around in memory, unencrypted, with absolutely no control over when that memory is overwritten.
The SecureString class maintains an automatically encrypted buffer for storing string data, the contents of which can be decrypted and written to unmanaged memory using the Marshal class' SecureStringToGlobalAllocUnicode method for just long enough to make the logon call, before that memory is zeroed out and freed by Marshal.ZeroFreeGlobalAllocUnicode (in a finally clause, to make sure it happens). Notice that I specify CharSet.Unicode in my declaration, but Marshal does provide ANSI equivalents.
I use the SetLastError property of the DllImportAttribute to instruct the marshalling code to allow me to retrieve specific failure information using Marshal.GetLastWin32Error.
I use the PermissionSetAttribute to ensure that any code directly or indirectly calling the UserImpersonator constructor has full trust, as defined by the machine's Code Access Security Policy. The security implications of user impersonation make this a sensible safeguard.

No comments:

Post a comment