Safe Impersonation With Whidbey

Over the last couple of days we've talked about how to impersonate another user, and some security issues to keep in mind while impersonating.  Now I'd like to take a look at some new features available in Whidbey which can make the whole process much nicer.  I'm going to code this up in Visual Basic to take advantage of VB's ability to provide exception filters, which allow me to undo impersonation on the first pass of exception handling without actually catching the exception.

To start with, I'm going to make a SafeHandle wrapper around the user token, so that I gain all the benefits presented by the new SafeHandle model.  (More information on SafeHandles here and here).

Friend NotInheritable Class SafeUserToken
    Inherits SafeHandle

    <SuppressUnmanagedCodeSecurity()> _
    <ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)> _
    Private Declare Function CloseHandle Lib "Kernel32" (ByVal hObject As IntPtr) As Boolean

    Private Sub New()
        MyBase.New(IntPtr.Zero, True)
    End Sub

    Public Overrides ReadOnly Property IsInvalid() As Boolean
        Get
            Return IntPtr.Zero.Equals(handle)
        End Get
    End Property

    Protected Overrides Function ReleaseHandle() As Boolean
        Return CloseHandle(handle)
    End Function
End Class

Now, I'll define a delegate that will be called under the impersonation context.  The delegate takes a single parameter which is a generic type.  The return value will also be generic.

Public Delegate Function ImpersonationWorkFunction(Of TReturn, TParameter)(ByVal paramter As TParameter) As TReturn

For the actual work of the utility library, I'm going to have a static method that takes:

  • the user name
  • domain
  • password (As a SecureString in order to take advantage of all the extra security benefits it provides)
  • logon type
  • logon provider
  • a delegate to run in the impersonated context
  • and a parameter to pass to the delegate

The return value of the method will be whatever the delegate returns.  Essentially this method is a wrapper around the code exposed by the delegate which makes it run while impersonating.  The code for this method is a pretty straightforward fallout of our last post.

<SecurityPermission(SecurityAction.Demand, UnmanagedCode := True)> _
Public Shared Function Impersonate(Of TReturn, TParameter)(ByVal userName As String, ByVal domain As String, _
  ByVal password As SecureString, ByVal parameter As TParameter, _
  ByVal impersonationWork As ImpersonationWorkFunction(Of TReturn, TParameter), _
  ByVal logonMethod As LogonType, ByVal provider As LogonProvider) As TReturn
    ' Check the parameters
    If String.IsNullOrEmpty(userName) Then
        Throw New ArgumentNullException("userName")
    End If
    If password Is Nothing Then
        Throw New ArgumentNullException("password")
    End If
    If impersonationWork = Nothing Then
        Throw New ArgumentNullException("impersonationWork")
    End If
    If logonMethod < LogonType.Interactive Or LogonType.NewCredentials < logonMethod Then
        Throw New ArgumentOutOfRangeException("logonMethod")
    End If
    If provider < LogonProvider.DefaultProvider Or LogonProvider.WinNT50 < provider Then
        Throw New ArgumentOutOfRangeException("provider")
    End If

    Dim passwordPointer As IntPtr = IntPtr.Zero
    Dim token As SafeUserToken = Nothing
    Dim context As WindowsImpersonationContext = Nothing

    Try
        ' convert the password to a unicode string
        passwordPointer = Marshal.SecureStringToGlobalAllocUnicode(password)

        ' get a user token
        If Not LogonUserW(userName, domain, passwordPointer, logonMethod, provider, token) Then
            Throw New Win32Exception(Err.LastDllError)
        End If
    Finally
        ' Erase the memory that the password was stored in
        If Not IntPtr.Zero.Equals(passwordPointer) Then
            Marshal.ZeroFreeGlobalAllocUnicode(passwordPointer)
        End If
    End Try

    Try
        ' Impersonate
        Debug.Assert(token IsNot Nothing)
        context = WindowsIdentity.Impersonate(token.DangerousGetHandle())

        ' Call out to the work function
        Return impersonationWork(parameter)
    Catch When UndoImpersonation(token, context) = False
        Debug.Assert(False, "UndoImpersonation returned False")
    Finally
        UndoImpersonation(token, context)
    End Try
End Function

Private Shared Function UndoImpersonation(ByRef token As SafeUserToken, ByRef context As WindowsImpersonationContext) As Boolean
    If context IsNot Nothing Then
        context.Undo()
        context = Nothing
    End If
    If token IsNot Nothing Then
        token.Dispose()
        token = Nothing
    End If

    Return True
End Function

Basically, we start by calling LogonUser.  Then we begin impersonating and call the delegate.  If the delegate were to throw, we undo the impersonation in the exception filter.  If it doesn't throw, the finally block takes care of undoing impersonation.

Finally, this method would be made slightly easier to use if we provided an overload that defaulted the logon type to Interactive and the logon provider to DefaultProvider.

Public Shared Function Impersonate(Of TReturn, TParameter)(ByVal userName As String, ByVal domain As String, _
 ByVal password As SecureString, ByVal parameter As TParameter, _
 ByVal impersonationWork As ImpersonationWorkFunction(Of TReturn, TParameter)) As TReturn

    Return Impersonate(userName,  _
      domain, _
      password, _
      parameter, _
      impersonationWork, _
      LogonType.Interactive, _
      LogonProvider.DefaultProvider)
End Function

We can compile all of that infrastructure into a utility library.  Using that library will make safely completing some work in an impersonated context pretty easy, especially when you take advantage of C#'s new support for anonymous delegates:

WindowsImpersonation.Impersonate<object, object>("SomeOtherUser", "MYDOMAIN", password, null, delegate
{
    Console.WriteLine("Impersonating {0}", WindowsIdentity.GetCurrent().Name);
    return null;
});

Perhaps a more real-world usage might be something like this:

string fileContents =
WindowsImpersonation.Impersonate<string, string>("AdminUser", "MYDOMAIN", password, @"c:\AdminStuff\File.txt", delegate(string file)
{
    using(StreamReader fileReader = new StreamReader(file))
        return fileReader.ReadToEnd();
});