Loading a User Profile from C#

Lightweight Impersonation using the LogonUser function may be nice for accessing a file or a database. It changes the credentials, but not the profile associated with the process. If you want access the impersonated account's mapped network drives, printers, environment variables and special folders (e.g. DeskTop, My Documents, and Application Data Folders) then you need to load its profile.

Changing the profile associated with a process is not obvious. The win32 APIs contain functions to do that: theoretically you could -after the impersonation for the current process started- respectively call ImpersonateLoggedOnUser to put the security context on the thread, LoadUserProfile to load the User Hive and CreateEnvironmentBlock to get the environment variables for the specified user. Unfortunately you need to have an unrealistic set of permissions to make this work (other than to a specific account for a Windows Service, I would be reluctant to assign 'Administrator' as well as 'Part of the Operating System' privileges).

So when you want to load a specific account's user profile, it makes probably more sense to simply start another process under that account. The ProcessStartInfo structure -used as parameter of .NET's Process.Start() method- has all the necessary attributes to make sure the account's profile is loaded.

Here's some sample code; it's the Main method of a console application that restarts itself under another account:

    ////
    // Process Restart Demo: Restart process under another account
    ////
 
    Console.Write("User Name: ");
    Console.WriteLine(WindowsIdentity.GetCurrent().Name);
    Console.Write("Application Directory: ");
    Console.WriteLine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData));
 
    // Create Impersonation Object
    Impersonation impersonation = new Impersonation("YourDomain", "Test123", "Test123");
 
    // Ask for Restart
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.Write("\nReady to restart (Y/N): ");
    ConsoleKeyInfo key = Console.ReadKey();
    if ((key.KeyChar == 'y') || (key.KeyChar == 'Y'))
    {
        // Start Process
        try
        {
            impersonation.StartProcess(Assembly.GetExecutingAssembly().Location);
        }
        catch (Exception ex)
        {
            // Oops
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("\n\n" + ex.Message);
            Console.ReadKey();
        }
    }


Here's the full Impersonation class:

//-----------------------------------------------------------------------
// <copyright file="Impersonation.cs" company="DockOfTheBay">
//     http://www.dotbay.be
// </copyright>
// <summary>Defines the Impersonation class.</summary>
//-----------------------------------------------------------------------
 
namespace DockOfTheBay
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Security;
    using System.Security.Principal;
 
    /// <summary>
    /// Facilitates impersonation of a Windows User.
    /// </summary>
    public class Impersonation
    {
        /// <summary>
        /// The User Id.
        /// </summary>
        private string userName = string.Empty;
 
        /// <summary>
        /// The Domain.
        /// </summary>
        private string domain = string.Empty;
 
        /// <summary>
        /// The Password.
        /// </summary>
        private SecureString password;
 
        /// <summary>
        /// Windows Token.
        /// </summary>
        private IntPtr tokenHandle = new IntPtr(0);
 
        /// <summary>
        /// The impersonated User.
        /// </summary>
        private WindowsImpersonationContext impersonatedUser;
 
        /// <summary>
        /// Initializes a new instance of the Impersonation class.
        /// </summary>
        /// <param name="domainName">Domain name of the impersonated user.</param>
        /// <param name="userName">Name of the impersonated user.</param>
        /// <param name="password">Password of the impersonated user.</param>
        /// <remarks>
        /// Uses the unmanaged LogonUser function to get the user token for
        /// the specified user, domain, and password.
        /// </remarks>
        public Impersonation(string domainName, string userName, string password)
        {
            // Fill private field for later use
            this.domain = domainName;
            this.userName = userName;
            this.password = new SecureString();
            char[] passwordChars = password.ToCharArray();
            foreach (char c in passwordChars)
            {
                this.password.AppendChar(c);
            }
 
            // Use the standard logon provider.
            const int LOGON32_PROVIDER_DEFAULT = 0;
 
            // Create a primary token.
            const int LOGON32_LOGON_INTERACTIVE = 2;
 
            this.tokenHandle = IntPtr.Zero;
 
            // Call LogonUser to obtain a handle to an access token.
            bool returnValue = LogonUser(
                                userName,
                                domainName,
                                password,
                                LOGON32_LOGON_INTERACTIVE,
                                LOGON32_PROVIDER_DEFAULT,
                                ref this.tokenHandle);
 
            if (false == returnValue)
            {
                // Something went wrong.
                int ret = Marshal.GetLastWin32Error();
                throw new System.ComponentModel.Win32Exception(ret);
            }
        }
 
        /// <summary>
        /// Starts the impersonation.
        /// </summary>
        public void Impersonate()
        {
            // Create Identity.
            WindowsIdentity newId = new WindowsIdentity(this.tokenHandle);
 
            // Start impersonating.
            this.impersonatedUser = newId.Impersonate();
        }
 
        /// <summary>
        /// Stops the impersonation and releases security token.
        /// </summary>
        public void Revert()
        {
            // Stop impersonating.
            if (this.impersonatedUser != null)
            {
                this.impersonatedUser.Undo();
            }
 
            // Release the token.
            if (this.tokenHandle != IntPtr.Zero)
            {
                CloseHandle(this.tokenHandle);
            }
        }
 
        /// <summary>
        /// Spawns a process under a specific account.
        /// </summary>
        /// <param name="fullPath">Full path to the executable.</param>
        /// <remarks>There's no need to call 'Impersonate' first.</remarks>
        public void StartProcess(string fullPath)
        {
            ProcessStartInfo info = new ProcessStartInfo();
 
            // File properties
            info.FileName = Path.GetFileName(fullPath);
            info.WorkingDirectory = Path.GetDirectoryName(fullPath);
 
            // User properties
            info.Domain = this.domain;
            info.UserName = this.userName;
            info.Password = this.password;
            info.LoadUserProfile = true; // Optional, but makes sense ...
            info.UseShellExecute = false; // Must be 'false' when 'UserName' is filled
 
            // Kick Off
            Process.Start(info);
        }
 
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool CloseHandle(IntPtr handle);
 
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool LogonUser(
                string lpszUsername,
                string lpszDomain,
                string lpszPassword,
                int dwLogonType,
                int dwLogonProvider,
                ref IntPtr phToken);
    }
}

1 comment:

  1. Nice, but once you're process is running as another user (B), how do you become to original user? For things like file browsing, won't you be looking at B's My Documents, Desktop, mapped drives?

    ReplyDelete