Wednesday, February 06, 2008

Safe(r) Impersonation in .Net 2.0

Impersonation is about assuming the identity of another security principal in order to perform operations with the principle's security context.  The first step in impersonation is obtaining proof that you have the right to assume another identity.  I chose to wrap the ADVAPI32.DLL export LogonUser which obtains a security token if proper credentials are passed and authenticated.   The wrapper class looks like this (and thanks to this post for the assist):

    internal class Impersonation
    {
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool LogonUser(
            string principal,
            string authority,
            string password,
            LogonSessionType logonType,
            LogonProvider logonProvider,
            out IntPtr token);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool CloseHandle(IntPtr handle);

        public static bool LogonUser(string userName, string domainName, string password, out IntPtr userToken)
        {
            return LogonUser(userName, 
                             domainName, 
                             password, 
                             LogonSessionType.Interactive, 
                             LogonProvider.WinNT50, 
                             out userToken);
        }

        public static int GetLastError()
        {
            return Marshal.GetLastWin32Error();
        }

        #region Nested type: LogonProvider

        private enum LogonProvider : uint
        {
            Default = 0, // default for platform
            WinNT35, // sends smoke signals to authority
            WinNT40, // uses NTLM
            WinNT50 // negotiates Kerb or NTLM
        }

        #endregion

        #region Nested type: LogonSessionType

        private enum LogonSessionType : uint
        {
            Interactive = 2,
            Network,
            Batch,
            Service,
            NetworkCleartext = 8,
            NewCredentials
        }

        #endregion
    }

I have a little dialog for soliciting credentials:

image

After calling Impersonation.LogonUser successfully, I have a valid security token with which I can impersonate.  .Net provides two classes to assist: WindowsIdentity and WindowsImpersonationContext.  As both implement IDisposable, one might write something like the following:

IntPtr userToken;

if (Impersonation.LogonUser(_userName, _domain, _password, out userToken))
{
    using (WindowsIdentity identity = new WindowsIdentity(userToken))
    using (WindowsImpersonationContext impersonationContext = identity.Impersonate())
    {
        try
        {
            GetServiceStatuses();
        }
        finally
        {
            impersonationContext.Undo();

            Impersonation.CloseHandle(userToken);
        }
    }
}

The problem here is that "finally" will execute EVENTUALLY, but there is the potential for an evil-doer to execute code while still impersonating. I did some reading, and I recommend this article by Eric Lippert here. Referenced deep in the comments is the CLI Standard.  For a deep dive into the CLI's Exception Model, visit this blog.

The implementation I settled on looks like the following (there are things that could be improved on here as well).   The key here is that the I still throw the exception to the calling code but I undo the impersonation before that happens.

  IntPtr userToken = IntPtr.Zero;
  WindowsIdentity identity = null;
  bool isImpersonationReversed  = false;
  WindowsImpersonationContext impersonationContext = null;
  
  try
  {

      if (Impersonation.LogonUser(_userName, _domain, _password, out userToken))
      {
          identity = new WindowsIdentity(userToken);
          impersonationContext = identity.Impersonate();
          
          GetServiceStatuses();
      }
  }
  catch(Exception exception)
  {
      if (impersonationContext != null)
      {                        
          impersonationContext.Undo();
          isImpersonationReversed = true;
      }                        
      throw;
  }
  finally
  {
      if (impersonationContext != null)
      {
          if (!isImpersonationReversed)
          {
              impersonationContext.Undo();
          }
          impersonationContext.Dispose();
      }

      if (identity != null)
      {
          identity.Dispose();
      }

      if (userToken != IntPtr.Zero)
      {
          Impersonation.CloseHandle(userToken);
      }                    
  }

 

Okay, back to work!

No comments:

Disclaimer

Content on this site is provided "AS IS" with no warranties and confers no rights. Additionally, all content on this site is my own personal opinion and does not represent my employer's view in any way.