Windows Identity Impersonation Made Easy(ish)

There are lots of different reasons why you would want to use identity impersonation in an application. The most common situation that I see is that you need to access a network resource, and you’ve been given a domain user account and password that has rights.

It would be nice to have an easy way to do the impersonation without having to do a lot of mucking around. I sat down to do this and came up with a nice, simple solution that does just that. I used this MSDN article as the basis for my solution, and there was a small amount of copying/pasting involved. I’m going to work my way backwards through the solution and then present the entire solution at the end.

What I wanted to have in the end was an extension method that could be used from any object. This was the goal usage:

var f = new Foo();
f.ExecuteAs(domainName, userName, password, x => x.Bar("Hi!"));

In order to have the extension method available, I needed to have a static class with a static method. The static method should accept the user credentials necessary to authenticate and the actual method to be executed.

public static class ExecuteAsExtension
{
    public static void ExecuteAs<T>(this T source, string domainName,
        string userName, string password, Action<T> action)
    {
        var token = GetToken(domainName, userName, password);
        using (token)
        {
            var identity = new WindowsIdentity(token.DangerousGetHandle());
            using (identity.Impersonate())
            {
                action(source);
            }
        }
    }
}

So that’s pretty easy! Now we just need to implement the GetToken method. This is the part of my solution that relies on the aforementioned MSDN article. I’ll just post the entire contents of my ExecuteAsExtension.cs file so that you can see it all:

using System;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Principal;
using Microsoft.Win32.SafeHandles;

namespace MyImpersonationNamespace
{
    public static class ExecuteAsExtension
    {
        [DllImport("advapi32.dll", SetLastError = true,
            CharSet = CharSet.Unicode)]
        public static extern bool LogonUser(String lpszUsername,
            String lpszDomain, String lpszPassword,
            int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public extern static bool CloseHandle(IntPtr handle);

        internal static SafeTokenHandle GetToken(string domainName,
            string userName, string password)
        {
            SafeTokenHandle safeTokenHandle;

            const int LOGON32_PROVIDER_DEFAULT = 0;
            //This parameter causes LogonUser to create a primary token.
            const int LOGON32_LOGON_INTERACTIVE = 2;

            // Call LogonUser to obtain a handle to an access token.
            bool returnValue = LogonUser(userName, domainName, password,
                LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
                out safeTokenHandle);

            if (false == returnValue)
            {
                int ret = Marshal.GetLastWin32Error();
                throw new System.ComponentModel.Win32Exception(ret);
            }

            return safeTokenHandle;
        }

        public static void ExecuteAs<T>(this T source, string domainName,
            string userName, string password, Action<T> action)
        {
            var token = GetToken(domainName, userName, password);
            using (token)
            {
                var identity = new WindowsIdentity(token.DangerousGetHandle());
                using (identity.Impersonate())
                {
                    action(source);
                }
            }
        }
    }

    public sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        private SafeTokenHandle()
            : base(true)
        {
        }

        [DllImport("kernel32.dll")]
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [SuppressUnmanagedCodeSecurity]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr handle);

        protected override bool ReleaseHandle()
        {
            return CloseHandle(handle);
        }
    }
}

To recap, using this new extension method to execute a function as another use requires just two steps:

  1. Add a using statement for your extension method’s namespace
  2. Use the extension method to execute a function

For reference, here is my test console application:

using System;
using System.Security.Principal;
using MyImpersonationNamespace;

namespace MyImpersonationNamespace
{
    class Program
    {
        static void Main(string[] args)
        {
            var domainName = "XP";
            var userName = "Administrator";
            var password = "SecretPassword";

            Console.WriteLine("Starting as {0}",
                WindowsIdentity.GetCurrent().Name);
            var f = new Foo();
            f.ExecuteAs(domainName, userName, password, x => x.Bar("dude"));
            Console.WriteLine("Finishing as {0}",
                WindowsIdentity.GetCurrent().Name);
            Console.ReadLine();
        }
    }

    class Foo
    {
        internal void Bar(string name)
        {
            Console.WriteLine("Executing as {0}",
                WindowsIdentity.GetCurrent().Name);
            Console.WriteLine("Hello, {0}!", name);
        }
    }
}

9/28/2012 Update:
Fixed a bug in the ExecuteAs methods above where the “Action action” argument should be “Action<T> action”
Also, here’s the static class implementation provided by ericrouse in his comment. Thanks, Eric!

public static void Action(SecurityKey securityKey, Action action)
{
	using (SafeTokenHandle token = GetToken(securityKey.Domain, securityKey.UserName, securityKey.Password))
	{
		WindowsIdentity identity = new WindowsIdentity(token.DangerousGetHandle());

		using (identity.Impersonate())
		{
			action();
		}
	}
}

/* Usage:
ExecuteAs.Action(SecurityKey,
    () =>
    {
        // Perform some action
    });
*/

11/5/2012 Update:
I was having some problems using this code to access a UNC path from within a unit test. The solution was to change the Logon Type parameter from “Interactive” to “New Credentials.”

const int LOGON32_LOGON_INTERACTIVE = 2;
const int LOGON32_LOGON_NEW_CREDENTIALS = 9;

bool returnValue = LogonUser(userName, domainName, password,
                LOGON32_LOGON_NEW_CREDENTIALS, 
                LOGON32_PROVIDER_DEFAULT,
                out safeTokenHandle);