How to Impersonate

Guillermo recently started blogging about some Whidbey enhancements around impersonation.  However, figuring out how to impersonate in the first place can be a little less than obvious.

WindowsIdentity contains an Impersonate method, but it doesn't accept any parameters.  That means that we'll need to supply the user and password through some other means.  The constructor for WindowsIdentity that takes a string looks promising, however there doesn't seem to be any way to get a password in.  Instead we'll have to use the constructor that takes a token (as an IntPtr).  If we intend to go that route, then we might as well just use the static WindowsIdentity.Impersonate overload that takes a token directly.

OK, so how do we get that token?  Well, we can P/Invoke to LogonUserLogonUser takes a user name, domain, and password, and returns us a token that we can use with WindowsIdentity.  In addition to these parameters, LogonUser wants to know which logon provider to use (the default should suffice for most cases), and a logon type  Selecting a logon type can be a tricky, so lets take a look at our options.

Logon Type(LOGON32_LOGON_xxx)

Integer Value(From WinBase.h) Associated Rights (From NTSecAPI.h) Description
BATCH 4 SeBatchLogonRight /SeDenyBatchLogonRight Perform a batch logon.  This is intended for servers where logon performance is vital.  LogonUser will not cache credentials for a user logged in with this type.
INTERACTIVE 2 SeInteractiveLogonRight /SeDenyInteractiveLogonRight Log a user into the computer if they will be interactively using the machine (for instance if your code is going to provide them with a shell).  LogonUser will cache credentials for a user logged in with this type.
NETWORK 3 SeBatchNetworkRight /SeDenyNetworkLogonRight Similar to batch logon in that this logon type is intended for servers where logon performance is important.  Credentials will not be cached, and the token returned is not a primary token but an impersonation token instead.  It can be converted to a primary token with DuplicateHandle.
NETWORK_CLEARTEXT 8 SeBatchNetworkRight /SeDenyNetworkLogonRight Similar to a network logon, except that the credentials are stored with the authentication package that validates the user.  This logon type is only available with the WINNT50 logon provider, and on Windows 2000 or higher.
NEW_CREDENTIALS 9 SeBatchNetworkRight /SeDenyNetworkLogonRight Create a new set of credentials for network connections.  (For instance runas /netonly).  This logon type is only available with the WINNT50 logon provider and on Windows 2000 or higher.
SERVICE 5 SeServiceLogonRight /SeDenyServiceLogonRight Logon as a service.
UNLOCK 7 Reserved for use by GINA implementations.

One important thing to note is that in order to use WindowsIdentity.Impersonate(), we need a primary token, so if we go with LOGON32_LOGON_NETWORK, we'll need to use DuplicateHandle in order to get at the primary token.  Since I like to avoid extra work like that, it looks like LOGON32_LOGON_BATCH or LOGON32_LOGON_INTERACTIVE would both be more appropriate choices.  Selecting between them will depend on the rights that the account we're trying to logon has.

Once your call to LogonUser has gotten you a user token for the user you'd like to impersonate, you can then call WindowsIdentity.Impersonate() and have your thread take over the identity of the Windows user you just logged on.  The code might look something like this:

// Call LogonUser to get a token for the user
IntPtr userHandle = IntPtr.Zero();
bool loggedOn = LogonUser(
    user,
    domain,
    password,
    LogonType.Interactive,
    LogonProvider.Default,
    out userHandle);
if(!loggedOn)
    throw new Win32Exception(Marshal.GetLastWin32Error());

// Begin impersonating the user
WindowsImpersonationContext impersonationContext = WindowsIdentity.Impersonate(userHandle.Token);

DoSomeWorkWhileImpersonating();

// Clean up
CloseHandle(userHandle);
impersonationContext.Undo();

However this code has a very subtle security issue that's just waiting to bite you.  Before I fix the problem tomorrow,  any guesses as to what might be wrong with the code?

Update 4:35 PM: Corrected an issue that wasn't the bug I'm referring to above.