13

Why impersonation user context is available only until the async method call? I have written some code (actually based on Web API) to check the behavior of the impersonated user context.

async Task<string> Test()
{
    var context = ((WindowsIdentity)HttpContext.Current.User.Identity).Impersonate();
    await Task.Delay(1);
    var name = WindowsIdentity.GetCurrent().Name;
    context.Dispose();
    return name;
}

To my surprise in this situation I will receive the name of the App pool user. under which the code is running. That means that I don't have the imprsonated user context anymore. If the delay is changed to 0, which makes the call synchronous:

async Task<string> Test()
{
    var context = ((WindowsIdentity)HttpContext.Current.User.Identity).Impersonate();
    await Task.Delay(0);
    var name = WindowsIdentity.GetCurrent().Name;
    context.Dispose();
    return name;
}

Code will return the name of currently impersonated user. As far as I understand the await and what debugger shows as well, the context.Dispose() is not called until name is being assigned.

noseratio
  • 59,932
  • 34
  • 208
  • 486
Paweł Forys
  • 347
  • 1
  • 3
  • 15
  • 2
    You've impersonated on some random thread pool thread. The next request to run on it might be affected by this. Super dangerous. – usr Jul 20 '15 at 21:34
  • 1
    @usr, as it turns, it's not that dangerous unless you impersonate inside something like `UnsafeQueueUserWorkItem`. Otherwise, the identity gets correctly propagated and restored, it won't be left hanging on a pool thread. See [this little experiment](https://gist.github.com/noserati/940c21b488e59d502dd1), particularly `GoThruThreads`. It's even more safe in ASP.NET, check my update. – noseratio Jul 21 '15 at 02:57
  • @Noseratio good to know. – usr Jul 21 '15 at 09:16

2 Answers2

14

In ASP.NET, WindowsIdentity doesn't get automatically flowed by AspNetSynchronizationContext, unlike say Thread.CurrentPrincipal. Every time ASP.NET enters a new pool thread, the impersonation context gets saved and set here to that of the app pool user. When ASP.NET leaves the thread, it gets restored here. This happens for await continuations too, as a part of the continuation callback invocations (those queued by AspNetSynchronizationContext.Post).

Thus, if you want to keep the identity across awaits spanning multiple threads in ASP.NET, you need to flow it manually. You can use a local or a class member variable for that. Or, you can flow it via logical call context, with .NET 4.6 AsyncLocal<T> or something like Stephen Cleary's AsyncLocal.

Alternatively, your code would work as expected if you used ConfigureAwait(false):

await Task.Delay(1).ConfigureAwait(false);

(Note though you'd lose HttpContext.Current in this case.)

The above would work because, in the absence of synchronization context, WindowsIdentity does gets flowed across await. It flows in pretty much the same way as Thread.CurrentPrincipal does, i.e., across and into async calls (but not outside those). I believe this is done as a part of SecurityContext flow, which itself is a part of ExecutionContext and shows the same copy-on-write behavior.

To support this statement, I did a little experiment with a console application:

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        static async Task TestAsync()
        {
            ShowIdentity();

            // substitute your actual test credentials
            using (ImpersonateIdentity(
                userName: "TestUser1", domain: "TestDomain", password: "TestPassword1"))
            {
                ShowIdentity();

                await Task.Run(() =>
                {
                    Thread.Sleep(100);

                    ShowIdentity();

                    ImpersonateIdentity(userName: "TestUser2", domain: "TestDomain", password: "TestPassword2");

                    ShowIdentity();
                }).ConfigureAwait(false);

                ShowIdentity();
            }

            ShowIdentity();
        }

        static WindowsImpersonationContext ImpersonateIdentity(string userName, string domain, string password)
        {
            var userToken = IntPtr.Zero;
            
            var success = NativeMethods.LogonUser(
              userName, 
              domain, 
              password,
              (int)NativeMethods.LogonType.LOGON32_LOGON_INTERACTIVE,
              (int)NativeMethods.LogonProvider.LOGON32_PROVIDER_DEFAULT,
              out userToken);

            if (!success)
            {
                throw new SecurityException("Logon user failed");
            }
            try 
            {           
                return WindowsIdentity.Impersonate(userToken);
            }
            finally
            {
                NativeMethods.CloseHandle(userToken);
            }
        }

        static void Main(string[] args)
        {
            TestAsync().Wait();
            Console.ReadLine();
        }

        static void ShowIdentity(
            [CallerMemberName] string callerName = "",
            [CallerLineNumber] int lineNumber = -1,
            [CallerFilePath] string filePath = "")
        {
            // format the output so I can double-click it in the Debuger output window
            Debug.WriteLine("{0}({1}): {2}", filePath, lineNumber,
                new { Environment.CurrentManagedThreadId, WindowsIdentity.GetCurrent().Name });
        }

        static class NativeMethods
        {
            public enum LogonType
            {
                LOGON32_LOGON_INTERACTIVE = 2,
                LOGON32_LOGON_NETWORK = 3,
                LOGON32_LOGON_BATCH = 4,
                LOGON32_LOGON_SERVICE = 5,
                LOGON32_LOGON_UNLOCK = 7,
                LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
                LOGON32_LOGON_NEW_CREDENTIALS = 9
            };

            public enum LogonProvider
            {
                LOGON32_PROVIDER_DEFAULT = 0,
                LOGON32_PROVIDER_WINNT35 = 1,
                LOGON32_PROVIDER_WINNT40 = 2,
                LOGON32_PROVIDER_WINNT50 = 3
            };

            public enum ImpersonationLevel
            {
                SecurityAnonymous = 0,
                SecurityIdentification = 1,
                SecurityImpersonation = 2,
                SecurityDelegation = 3
            }

            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern bool LogonUser(
                    string lpszUsername,
                    string lpszDomain,
                    string lpszPassword,
                    int dwLogonType,
                    int dwLogonProvider,
                    out IntPtr phToken);

            [DllImport("kernel32.dll", SetLastError=true)]
            public static extern bool CloseHandle(IntPtr hObject);
        }
    }
}

**Updated**, as @PawelForys suggests in the comments, another option to flow impersonation context automatically is to use `` in the global `aspnet.config` file (and, if needed, `` as well, e.g. for `HttpWebRequest`).
phillhutt
  • 183
  • 1
  • 5
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 3
    Thank you very much for the deep answer. It really helped me understood what stays behind the context passed to and from async calls. I have found another solution that can change the default behavior and allow passing identity to the eventual threads created by async. Which is explained in here: http://stackoverflow.com/a/10311823/637443. The settings: and should also work for applications using app.config. Using this config, identity will be passed and preserved. Please correct if that is not true. – Paweł Forys Jul 21 '15 at 17:09
  • 1
    @PawełForys, I think `` alone should do it, you probably don't need `legacyImpersonationPolicy`. Let us know if that works. – noseratio Jul 21 '15 at 19:47
  • 1
    Correct, alone allows flow of the identity across threads. Thanks! – Paweł Forys Jul 22 '15 at 09:50
  • 1
    if you don't mind please update your answer, as you mentioned that "if you want to keep the identity across awaits spanning multiple threads in ASP.NET, you need to flow it manually". When using , that is being done automatically. Thanks! – Paweł Forys Jul 22 '15 at 09:54
  • however is necessary to send impersonated calls by HttpWebRequest for instance. – Paweł Forys Jul 22 '15 at 12:09
  • please update your message if possibe, as and will not be taken into account if placed in web.config. Those settings need to be set on global aspnet.config (or app pool specific config: http://weblogs.asp.net/owscott/setting-an-aspnet-config-file-per-application-pool). Thanks – Paweł Forys Jul 24 '15 at 08:19
  • 1
    Are there any security risks to know about by turning on the alwaysFlowImpersonationPolicy? – eaglei22 Jul 05 '17 at 19:02
  • Seems like now there is official docs regarding the issue: https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/legacyimpersonationpolicy-element#remarks – Feofilakt Jan 18 '22 at 09:10
2

It appears that in case of using impersonated async http calls via httpWebRequest

HttpWebResponse webResponse;
            using (identity.Impersonate())
            {
                var webRequest = (HttpWebRequest)WebRequest.Create(url);
                webResponse = (HttpWebResponse)(await webRequest.GetResponseAsync());
            }

the setting <legacyImpersonationPolicy enabled="false"/> needs also be set in aspnet.config. Otherwise the HttpWebRequest will send on behalf of app pool user and not impersonated user.

Paweł Forys
  • 347
  • 1
  • 3
  • 15