Introduction
This article demonstrates a simple C++ class that provides support for User Impersonation, allowing an application to impersonate any other user (with automatic cleanup). The ImpersonateUser
class is a thin wrapper (utilising RAII) around the LogonUser()
, ImpersonateLoggedOnUser()
and RevertToSelf()
Windows API functions.
The Demo Project
The demo project included with this article is very simple, so don't expect it to be fantastic. It's just an example showing the class in use. It is a console application that takes several command line parameters, including the name/password of the user to impersonate. You can also specify a file name (which can be on a network share) that the demo project will attempt to access by impersonating the user details you specify. It will then indicate whether this was successful or not, as shown in the screen shots below.
What is User Impersonation?
User Impersonation allows an application to execute a task using the security context of another user. For example, a service running as LocalSystem
could access network resources by impersonating a specific user account. This account would have been configured with the necessary permissions to access a network resource, something the service would not be able to do otherwise.
Operating System Support
Windows 9x
ImpersonateUser
is not supported on Windows 95, 98 or ME as those operating systems do not support users or user security tokens. For impersonation to work, the operating system must support Kerberos security providers, such as those provided by Windows NT/2000 and above.
Windows NT/2000
To use the LogonUser
API on Windows NT/2000, the calling thread must have the SE_TCB_NAME
privilege. This is gained by ensuring that the user can "Act as part of the Operating System," which can be configured through the User Manager in the Windows Control Panel. Alternatively, the application can request the SE_TCB_NAME
privilege itself. This privilege is automatically granted to Windows Services running as the LocalSystem
account. This is the recommended way of using LogonUser
, i.e. from a Windows Service.
If the calling thread does not hold this privilege, then the LogonUser
call will fail, with GetLastError()
returning ERROR_PRIVILEGE_NOT_HELD
.
Windows XP, 2K, 2K3, Vista, etc.
With the introduction of Windows XP, the requirement to hold the SE_TCB_NAME
privilege was removed.
Background
The main reason for writing this class was that I needed to support impersonation in a project. I always try to write reusable code and so I created ImpersonateUser
, which wraps up the necessary calls to perform the impersonation.
The class's destructor provides automatic cleanup, so the easiest way to use the class is to create an instance of it on the stack. If you prefer dynamically allocating the class, then you should use exception handling to ensure proper cleanup (or of course a smart pointer wrapper, such as one of several provided by the Boost
library). I recommend instantiating the class on the stack, as you can then control the scope of the impersonation more easily.
I realise that there are several articles on Impersonation already published on The Code Project. However, none are using straight C++ as far as I can tell, although if there are some, I am sure someone will point this out to me.
The LogonUser API
LogonUser
can only be used to log onto the local machine; it cannot log you onto a remote computer. The account that you use in the LogonUser()
call must also be known to the local machine, either as a local account or as a domain account that is visible to the local computer. If LogonUser
is successful, then it will give you an access token that specifies the credentials of the user account you chose.
Types of Token
LogonUser()
can return two different types of tokens: primary
and impersonation
.
- Primary Token: This token is typically assigned to a process and becomes the default security token for that process.
- Impersonation Token: This token is used to obtain the security information for a client process, allowing you to impersonate the client process when performing security-related tasks such as accessing a shared folder on a network.
Usually, the token returned from LogonUser
is a primary token which can be used to create processes as another user, using the CreateProcessAsUser()
API. If you specify LOGON32_LOGON_NETWORK
as the LogonType
(see below) for the LogonUser()
call, then you will be given an Impersonation token instead of the default Primary Token. You cannot use this in a call to CreateProcessAsUser()
to impersonate a user unless you first convert it to a Primary Token using the DuplicateTokenEx()
API call.
You can then pass this token to ImpersonateLoggedOnUser()
, which will allow the calling thread to impersonate the security context of a logged-on user. This can be useful if you need to allow a Windows Service to access a network resource without wanting to give full permission to everyone to access that resource. You would simply give a specific user account the permissions necessary to access the network resource and get the Windows Service to impersonate that user account, access the resource and then undo the impersonation by reverting to the security context that the Service was originally running in.
The following sections explain the most important parameters in a call to the Windows API function LogonUser()
. I am not going to describe every supported value for these parameters, only the ones that relate directly to user impersonation. For a complete explanation of the remaining types, please refer to the MSDN documentation.
Username Parameter: lpszUsername
This specifies the name of the user to authenticate, i.e. the name of the user to log on as. If you enter this in user principal name (UPN) format -- e.g. accountname@example.com -- then the lpszDomain
parameter must be NULL
.
Domain Parameter: lpszDomain
This specifies the name of the domain used to authenticate the user's account. This can be the domain or the name of a workstation or server on the network. If this parameter is NULL
, then the lpszUsername
parameter must be specified in the UPN format. The easiest way to use LogonUser
is to specify the user name and domain as separate parameters, in which case, the domain name should be specified in NetBIOS
format, e.g. MyDomain
. To validate the user account using only the local system account database, specify "."
as the domain parameter.
Password Parameter: lpszPassword
To reduce the risk of compromised passwords, you should clear the password from memory as soon as you have called LogonUser()
. Do not rely on memset()
for doing this, as it can easily be removed from your code by an optimising compiler. You should use the SecureZeroMemory()
function instead, as this will not be optimised out.
LogonType Parameter: dwLogonType
This parameter controls the type of logon performed by LogonUser()
.
LOGON32_LOGON_BATCH
: Returns primary key. It is intended for servers that require high performance authentication and do not have to rely on credential caching, such as mail servers.LOGON32_LOGON_INTERACTIVE
: Returns Primary Token and requires the "Log on Locally" privilege. The user must have the right to log on locally on the target machine. User credentials are cached and so may have a slight performance hit when compared to LOGON32_LOGON_BATCH
.LOGON32_LOGON_NEW_CREDENTIALS
: Returns Primary Token. This type allows the caller to duplicate its current token and specify new credentials for outbound connections. It requires the LOGON32_PROVIDER_WINNT50
logon provider and is only supported on Windows 2000 and above.LOGON32_LOGON_NETWORK_CLEARTEXT
: Returns Primary Token and requires the LOGON32_PROVIDER_WINNT50
logon provider. It is only supported on Windows 2000 and above.LOGON32_LOGON_NETWORK
: Returns Impersonation Token. This is a high-speed authentication method and requires the "Access this computer from the network" privilege.
The main difference between LOGON32_LOGON_NETWORK
and LOGON32_LOGON_NETWORK_CLEARTEXT
is that LOGON32_LOGON_NETWORK_CLEARTEXT
will allow the authentication provider on the local machine to cache the login information you have specified. This allows that machine to impersonate on your behalf if it has to use it for any other network operation. It does not mean that the password is sent over the network unencrypted.
If you do not intend to impersonate the logged-on user, then you should use the LOGON32_LOGON_NETWORK
logon type. This is because it is the fastest authentication type, as the authentication provider does not cache any credentials. Despite its name, the LOGON32_LOGON_NETWORK
type returns a token that does not support access to network resources. It is, however, the fastest method to use if all you wish to do is authenticate a user's security details. If you convert a LOGON32_LOGON_NETWORK
logon into a primary token and then use it to start a process with CreateProcessAsUser()
, then the new process will not be able to access network resources unless the network resource is not access-controlled.
LogonProvider Parameter: dwLogonProvider
This parameter is responsible for specifying the name of the logon provider to use for authenticating the user's security details. You must ensure that all of the domain controllers on your network support the same logon provider. Your application could contact a domain controller for authentication and would fail because that specific domain controller does not support the requested authentication provider. This is primarily an issue with the LOGON32_LOGON_NEW_CREDENTIALS
and LOGON32_LOGON_NETWORK_CLEARTEXT
providers, as they are not supported on Windows NT.
LOGON32_PROVIDER_DEFAULT
: Use the default logon provider for the system based on the version of the operating system running on the domain controller. This will work with Windows NT 4 and newer domain controllers, but if you have any Windows NT 3.51 controllers on your domain, then you should use LOGON32_PROVIDER_WINNT35
. This uses the negotiate provider unless you pass NULL
for the domain and the user name is not in user principal name (UPN) format (e.g. accountname@example.com), in which case, the provider defaults to NTLM
.LOGON32_PROVIDER_WINNT35
: Use this provider if you'll be authenticating against a Windows NT 3.51 domain controller (uses the NT 3.51 logon provider).LOGON32_PROVIDER_WINNT40
: Use this provider if you'll be authenticating against a Windows NT 4 domain controller (uses the NTLM logon provider).LOGON32_PROVIDER_WINNT50
: Use the negotiate logon provider, supported by Windows XP and above.
Unexpected Behaviour: Guest User Accounts
An important thing to remember (as it may seem a little strange initially) is that LogonUser
will succeed if you pass in a bad username and password, but only if the guest account is enabled (with no password) on the domain. This can cause confusion when you first work with LogonUser
, as it can correctly authenticate an invalid logon attempt when you have specified both an invalid user name and password. The safest approach is to disable the Guest account and ensure it has a non-empty password.
Example Test Cases
This section shows a few test cases I used when testing the sample code, and the results under different circumstances. It shows how to use the ImpersonateUser
utility, as well as the results you should expect to see.
Case 1: Successful Impersonation - File found
In this example we specify a username, password and the name of a file that we will attempt to access. The impersonation was successful and the file was opened even though the user "darka" did not exist on the system. This was due to the "guest" account being enabled on the test PC with no password set on it.
Case 2: Successful Impersonation - File not found
In this example, we specify a valid username, password and the name of a file that does not exist. The impersonation was successful, but we failed to open the file, which is what we expected.
Case 3: Impersonation Failed
This example shows an attempt where we specify an invalid domain name, with everything else being valid. The impersonation failed, so the attempt to open the file was aborted.
The Code
This sample is a slightly cut-down version of the header file for the class, showing the methods available.
namespace darka
{
class ImpersonateUser
{
private:
bool init_;
HANDLE userToken_;
DWORD errorCode_;
public:
ImpersonateUser() : init_(false), userToken_(NULL), errorCode_(0) {}
~ImpersonateUser();
bool Logon(const std::string& userName,
const std::string& domain, const std::string& password);
void Logoff();
DWORD GetErrorCode() const { return errorCode_; }
};
}
Using the Code
ImpersonateUser obLogon;
if(!obLogon.Logon(userName, domain, password))
{
const string szErr = FormatSysError(obLogon.GetErrorCode());
cout << _T("\tUser Impersonation Failed!\r\n\t");
cout << szErr;
return -1;
}
else
cout << _T("\tUser Impersonated Successfully\r\n\t");
Points of Interest
None really, except that the code should compile cleanly under warning level 4 (Visual Studio 2003) and with 0 warnings/errors with PCLint.
References
History
- 1.20 (Feb 15, 2008) - Uploaded new versions of the zip files (encoded with 7Zip) as several people were having difficulty unzipping the originals.
- 1.20 (Oct 14, 2007) - Prepared the article for posting on The Code Project
- 1.10 (July 26, 2007) - Initial public release of article and updated wrapper class, adding STL support
- 1.00 (Sept 13, 2004) - Wrote initial wrapper class