Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async method call and impersonation

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.

like image 301
Paweł Forys Avatar asked Jul 20 '15 17:07

Paweł Forys


People also ask

What happens when you call an async method?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

What are async methods?

An async method runs synchronously until it reaches its first await expression, at which point the method is suspended until the awaited task is complete. In the meantime, control returns to the caller of the method, as the example in the next section shows.

Is async executed in another thread?

No, it does not. It MAY start another thread internally and return that task, but the general idea is that it does not run on any thread.

What is impersonation C#?

The term "Impersonation" in a programming context refers to a technique that executes the code under another user context than the user who originally started an application, i.e. the user context is temporarily changed once or multiple times during the execution of an application.


2 Answers

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`).
like image 140
noseratio Avatar answered Sep 28 '22 17:09

noseratio


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.

like image 32
Paweł Forys Avatar answered Sep 28 '22 17:09

Paweł Forys