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 removedAnonymous
April 06, 2005
The comment has been removedAnonymous
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 ThanksAnonymous
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