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.
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.
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.
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.
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.
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);
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With