Thursday, September 21, 2006

Explicit Impersonation and Async Thread Identity in ASP.NET

I've been doing some asynchronous thread programming in our server components and last night ran into a gotcha that took me a few hours to figure out so I thought I'd post this one to save people some trouble.

I've implemented a Fire and Forget asynchronous pattern similar to Mike Woodring's sample so that I could execute longer running processes than the typical web request on a spearate thread and return back to the client immediately. These processes connect to a SQL database using windows integrated security.

All was working just dandy on my development XP box running without impersonation. In this scenario the components run under the default ASPNET identity and connect to a local database. (In Windows 2003/IIS6 the default identity is NETWORK SERVICE.) However when I went to deploy it on our testing servers I ran into a problem with the asynchronous thread identities. They were throwing exceptions trying to connect to the database.

Our test rig is set up as two Windows 2003 servers, one app server and one database server on their own little domain. We set the app server's web.config with explicit impersonation of a least priveledged domain account that is windows authenticated to the database. All works fine for client request threads, however the identity of the async threads were that of the application pool, not the explicit domain user and was therefore causing problems connecting to the database. I figured I could change the application pool identity, but I wasn't satisfied with having to remember another configuration setting. I really wanted the Web.config to be the only place for this and I was perplexed as to why the main thread's identity wasn't getting propagated.

I still am not sure as to why this is the case since the documentation for ThreadPool.QueueUserWorkItem makes it seem like this should work in .NET 2.0. I ended up augmenting the AsynHelper class to impersonate a WindowsIdentity. Here's the code and it's usage (comments/suggestions welcome!):
Imports System.Threading
Imports System.Security

Friend Class AsyncHelper

    Private Shared wc As New WaitCallback(AddressOf CallMethod)

    Public Shared Sub FireAndForget(ByVal d As [Delegate], _
            ByVal wi As Principal.WindowsIdentity, _
            ByVal ParamArray args As Object())

        ThreadPool.QueueUserWorkItem(wc, New TargetInfo(d, args, wi))
    End Sub

    Private Shared Sub CallMethod(ByVal o As Object)
        Dim ti As TargetInfo = DirectCast(o, TargetInfo)

        'This is necessary so this thread impersonates the 
        'calling thread's identity. This is important when 
        'running under ASP.NET explicit impersonation.
        ti.Identity.Impersonate()

        'Invoke the method, passing the arguments
        ti.Target.DynamicInvoke(ti.Args)
    End Sub

    Private Class TargetInfo
        Private m_target As [Delegate]
        Private m_args As Object()
        Private m_wi As Principal.WindowsIdentity

        ReadOnly Property Target() As [Delegate]
            Get
                Return m_target
            End Get
        End Property

        ReadOnly Property Args() As Object()
            Get
                Return m_args
            End Get
        End Property

        ReadOnly Property Identity() As Principal.WindowsIdentity
            Get
                Return m_wi
            End Get
        End Property

        Sub New(ByVal d As [Delegate], _
            ByVal args As Object(), _
            ByVal wi As Principal.WindowsIdentity)

            m_target = d
            m_args = args
            m_wi = wi
        End Sub
    End Class

End Class
And here's a usage example:
Private Delegate Sub ExecuteQueryDelegate(ByVal personID As Integer) 

Public Sub BeginFetch(ByVal personID As Integer)
    Dim dlgt As New ExecuteQueryDelegate(AddressOf ExecuteQuery)

    ' Initiate the asynchronous call.
    AsyncHelper.FireAndForget(dlgt, Principal.WindowsIdentity.GetCurrent(), personID)
End Sub

'This method runs on an asynchronous thread
Private Sub ExecuteQuery(ByVal personID As Integer) 
    Dim resultSet As DataTable = Me.LongRunningProcesses()
    Me.SaveResults(resultSet)
End Sub

3 comments:

Chris said...

Howdy. I think you can accomplish the same thing with

<configuration>
  <runtime>
    <alwaysFlowImpersonationPolicy enabled="true"/>
    <legacyImpersonationPolicy enabled="false"/>
  </runtime>
</configuration>

PS - You're beautiful.

warren said...

Hi Beth,

Thank you kindly. This is a beautiful piece of code.

It has helped in solving a long standing problem in a Windows app where we were impersonating on a primary thread in order to write to privileged directories.

Great stuff!

Warren

Anonymous said...

I am not able to impoersonate the Async calls. Here is what i have done. Please help.

1) I set this in Web.Config

< runtime >
< alwaysFlowImpersonationPolicy enabled="true"/ >
< legacyImpersonationPolicy enabled="false"/ >
< /runtime >
< system.web >
< identity impersonate="true"/ >


2) I called a SQL server endpoint webservice, Impersonate it with default credentials

CapturedFiles.sql_data _ep = new CapturedFiles.sql_datata();
_ep.UseDefaultCredentials = true;

3) Make a Async call to

_ep.GetDropDownItemsCompleted +=
new CapturedFiles.GetDropDownItemsCompletedEventHandler(ep_GetDropDownItemsCompleted);
try
{
_ep.GetDropDownItemsAsync("Release", "", "Release Call");
}

4) ERROR

void ep_GetDropDownItemsCompleted(object sender, CapturedFiles.GetDropDownItemsCompletedEventArgs e)
{
if (e.Error != null)
{
-- HERE I AM GETTING AN ERROR Login Failed
}