Share via


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();
});

Comments

  • Anonymous
    March 31, 2005
    The comment has been removed

  • Anonymous
    April 06, 2005
    The comment has been removed

  • Anonymous
    August 08, 2006
    Hey,

    I'm writing an application that requires me to authenticate as windows user account user to perform part of a process before resetting back to it's original context.

    Unfortunately I have no choice but to use C# exclusively. Your code uses some language specific classes that and so I am unable to directly translate into C#.

    Is there a C# version available?

    Many Thanks

  • Anonymous
    January 17, 2011
    Hi! Could you please post your definition of the LogonUserW method? I'm wondering how the definition must look like to allow SafeHandle instances as parameter 'phToken'. Thanks a lot!

  • Anonymous
    January 18, 2011
    Sure - you can find one here: clrsecurity.codeplex.com/.../47833 Just make sure you're passing the safe handle as an out parameter of the declaration. -Shawn