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
The
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