Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

[Security] - User Impersonation

4.89/5 (14 votes)
16 Feb 2008CPOL11 min read 1   6.2K  
A simple C++ wrapper class for User Impersonation
Screenshot - usage.gif

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.

Screenshot - success.gif

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.

Screenshot - fail.gif

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.

Screenshot - domain.gif

The Code

This sample is a slightly cut-down version of the header file for the class, showing the methods available.

C++
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();

            // Misc Methods
        DWORD GetErrorCode() const { return errorCode_; }
    };
} // namespace

Using the Code

C++
// Instantiate our Impersonate Class
ImpersonateUser obLogon;

    // Impersonate the user
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)