Apply Impersonation to Threading Using Visual Basic

Introduction

This article addresses a simple approach to applying impersonation to threading; by default when impersonation is applied, it does not extend to threads spawned after impersonation is set. It is, however, possible to apply impersonation such that threads spawned by the application are spawned under the impersonated user.

This article and the accompanying code will provide an example in which a user is impersonated and threads are subsequently spawned under the identity of the impersonated user.

Threading-window-in-windows8.gif

Figure 1: Demonstration Application

In order to run the application effectively, you will need to have a test user account on the machine so that you can impersonate that account. If you need to, open up the control panel, go to user accounts, and add another user to the machine.

Getting Started:

In order to get started, unzip the included project and save it to your hard drive. There is a single project included in the download.

solution-explorer-in-windows8.gif

Figure 2: Solution Explorer for the Demo Application

The application project is called "ImpersonationTest". This application contains a single class (Impersonation.vb) and a form used to test impersonation. If you want to use impersonation as provided herein, just copy the contents of the impersonation class into your project.

Running the Application

Given that you have at least one account to impersonate on your local system, run the application. Key in the domain (or machine name), the user ID, and the password.
To begin, click on the Impersonate With Threads button. This will set the login the user provided using the domain, user ID, and password provided and set up an impersonation context, and call impersonate on the windows identity that you have provided. Once that is done, the method called with set the current thread principal to the impersonated user. With that done, any threads spawned will run under the impersonated user's identity.

You can test that this is the case by clicking next on the button that is labeled, "Get Current Identities for All". That method will report on the current principal Windows identity and the current thread principal identity.

After confirming that both the current principal Windows identity and the current thread principal identity are both set to the impersonated user's account, click on the button labeled, "Undo User Impersonation". This will restore the current logged in user as the current principal Windows identity but it will do nothing to the current thread principal. Click on the "Get Current Identities for All" button again and you will see that the current principal Windows identity is set back to the logged in user but the current thread principal is still set to the impersonated user.

Now, click the button labeled, "Undo Threading Impersonation". After that, click the "Get Current Identities for All" button again and you will see that both the current principal Windows identity and the current thread principal are both set back to the current logged user.

You can now click the "Impersonate Without Threads" button and check the identities again. This button just sets impersonation to the supplied user but does not set the thread principal to match and so you will see any threads spawned from this state will run under the currently logged in user rather than the impersonated user.

Code: Impersonation.vb

The Impersonation class does the work of setting up and removing impersonation. The class uses four Win32 API calls to handle logging in a user and removing a logged in, impersonated
 user. These Win32 API calls are well documented and there is nothing unique about them as they are included in this project.

The class begins with a few imports related to Windows security, threading, and interop. I will point out here that the class must be declared as Friend. You can review the Win32 calls in the first region of the code.

Imports System.Web
Imports System.Security.Principal
Imports System.Security.Permissions
Imports System.Runtime.InteropServices
Imports System.Threading

Friend Class Impersonate

#Region "Win32 API Methods"

    Const LOGON32_LOGON_INTERACTIVE As Integer = 2
    Const LOGON32_PROVIDER_DEFAULT As Integer = 0

    Declare Function LogonUserA Lib "advapi32.dll" (ByVal lpszUsername As String, _
                            ByVal lpszDomain As String, _
                            ByVal lpszPassword As String, _
                            ByVal dwLogonType As Integer, _
                            ByVal dwLogonProvider As Integer, _
                            ByRef phToken As IntPtrAs 
Integer

    Declare Auto Function DuplicateToken Lib "advapi32.dll" ( _
                            ByVal ExistingTokenHandle As IntPtr, _
                            ByVal ImpersonationLevel As Integer, _
                            ByRef DuplicateTokenHandle As IntPtrAs 
Integer

    Declare Auto Function RevertToSelf Lib "advapi32.dll" () As Long
    Declare Auto Function CloseHandle Lib "kernel32.dll" (ByVal handle As IntPtrAs 
Long

#End Region

#Region "Default Constructor"

    Private Sub New()
        MyBase.New()
    End 
Sub

#End Region

The class methods region contains the methods of interest; the first function provided is entitled, "ImpersonateValidUser". It accepts three arguments as the user name, the domain, and the user's password. This function will establish the passed in user as the current Windows identity through impersonation if that user can successfully be authenticated. This function will not have impact upon any threads that are subsequently spawned following the establishment of the impersonation context and any such threads will run under the identity of the current logged in user.

#Region "Class Methods"

    ''' <summary>
    ''' Impersonate a user without allowing threads to run under the impersonated user
    ''' </summary>
    ''' <param name="userName"></param>
    ''' <param name="domain"></param>
    ''' <param name="password"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Friend Shared Function ImpersonateValidUser(ByVal userName As String, _
                                                ByVal domain As String,
                                                ByVal password As StringAs 
     
WindowsImpersonationContext

        Dim impersonationContext As WindowsImpersonationContext = Nothing
        Dim tempWindowsIdentity As WindowsIdentity
        Dim token As IntPtr = IntPtr.Zero
        Dim tokenDuplicate As IntPtr = IntPtr.Zero

        Try

            If RevertToSelf() Then
                If LogonUserA(userName, domain, password, LOGON32_LOGON_INTERACTIVE, _
                         LOGON32_PROVIDER_DEFAULT, token) <> 0 Then
                    If DuplicateToken(token, 2, tokenDuplicate) <> 0 Then
                        tempWindowsIdentity = New WindowsIdentity(tokenDuplicate)
                        impersonationContext = tempWindowsIdentity.Impersonate()
                    End If
                End If
            End If
            Return impersonationContext
        Catch ex As Exception
            EventLog.WriteEntry(Application.ProductName, ex.Message)
        Finally
            If Not tokenDuplicate.Equals(IntPtr.Zero) Then
                CloseHandle(tokenDuplicate)
            End If
            If Not token.Equals(IntPtr.Zero) Then
                CloseHandle(token)
            End If
        End 
Try

        Return Nothing

    End Function

The next method in the class will do exactly the same thing as is accomplished in the previous method but it will also set the current thread principal to the impersonated user. Threads spawned after this call is successfully made will run under the context of the impersonated user.

    ''' <summary>
    ''' Impersonate a user and set the thread current principal to that impersonated 
    
''' user.  Afterwards, you can launch threads that will run under the impersonated 
    ''' user.
    ''' </summary>
    ''' <param name="userName"></param>
    ''' <param name="domain"></param>
    ''' <param name="password"></param>
    ''' <returns></returns>
    ''' 
<remarks></remarks>
Friend Shared Function ImpersonateValidUserAndSetThreadPrincipal(ByVal userName AsStringByVal domain As StringByVal password As StringAs WindowsImpersonationContext
 
        Dim impersonationContext As WindowsImpersonationContext = Nothing
        Dim tempWindowsIdentity As WindowsIdentity
        Dim token As IntPtr = IntPtr.Zero
        Dim tokenDuplicate As IntPtr = IntPtr.Zero
        Dim user As IIdentity
        Dim principal As 
WindowsPrincipal

        Try

            If RevertToSelf() Then
                If LogonUserA(userName, domain, password, LOGON32_LOGON_INTERACTIVE, _
                         LOGON32_PROVIDER_DEFAULT, token) <> 0 Then
                    If DuplicateToken(token, 2, tokenDuplicate) <> 0 Then
                        tempWindowsIdentity = New WindowsIdentity(tokenDuplicate)
                        impersonationContext = tempWindowsIdentity.Impersonate()

                        ' apply impersonation to threading
                        user = New WindowsIdentity(token, "NTLM"
                        WindowsAccountType.Normal, True)
                        principal = New WindowsPrincipal(user)
                        Thread.CurrentPrincipal = principal

                    End If
                End If
            End If
            Return impersonationContext
        Catch ex As Exception
            EventLog.WriteEntry(Application.ProductName, ex.Message)
        Finally
            If Not tokenDuplicate.Equals(IntPtr.Zero) Then
                CloseHandle(tokenDuplicate)
            End If
            If Not token.Equals(IntPtr.Zero) Then
                CloseHandle(token)
            End If
        End 
Try

        Return Nothing
 
    End 
Function


The last method of interest in the class is used to restore the current thread principal to the logged in user after impersonation is removed.


    ''' <summary>
    
''' After calling undo on impersonation, call this method to restore the thread  
    ''' current principal to the logged on user.
    ''' </summary>
    ''' <remarks></remarks>
    Friend Shared Sub RestoreThreadPrincipal()

        Try
            Dim principal As WindowsPrincipal
            principal = New WindowsPrincipal(WindowsIdentity.GetCurrent)
            Thread.CurrentPrincipal = principal
        Catch ex As Exception
            EventLog.WriteEntry(Application.ProductName, ex.Message)
        End 
Try

    End Sub

#End Region

End Class

Code: Form1.vb

The code behind the demo application basically consists of the button click event handlers used to add and remove impersonation and to report on the status of that impersonation. 
The class begins with a few imports to support threading and Windows security.

Imports System.Security.Principal
Imports System.Security
Imports System.Security.Permissions
Imports System.Threading
Imports System.Text

''' <summary>
''' Test application used to confirm that threads are launched and started under 
''' an impersonated identity
''' 
</summary>
''' <remarks></remarks>
Public Class Form1

There are two class wide variables declared, one is an impersonation context object and the other is a thread.  

 #Region "Class Scope VVariable Declarations"

    Private IPers As System.Security.Principal.WindowsImpersonationContext
    Private t1 As 
Thread

#End Region

The constructor for the demonstration form sets the impersonation context object to nothing to begin with.

#Region "Constructor"

    Public Sub New()
        InitializeComponent()
        IPers = Nothing
    End 
Sub

#End Region

The remainder of the functions/subroutines in the demo form class are annotated below.

#Region "Event Handlers"

    ''' <summary>
    ''' Impersonates another authorized user and spawns a single thread under the 
    ''' impersonated identity.
    ''' 
    ''' To test this, if you do not have a user to impersonate on the machine, add a new 
    
''' user and provide that user's information in the form fields (domain, user id, and 
    ''' password)
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnImpersonate_Click(ByVal sender As System.ObjectByVal e As 
    System.EventArgsHandles btnImpersonate.Click

        Try
            IPers = Impersonate.ImpersonateValidUserAndSetThreadPrincipal(txtUserId.Text, 
            txtDomain.Text, txtPassword.Text)
            t1 = New Thread(AddressOf ThreadTask)
            t1.IsBackground = True
            t1.Start()
        Catch ex As Exception
            MessageBox.Show(ex.Message, "Impersonation Error")
        End 
Try

    End Sub

    ''' <summary>
    
''' Impersonates a user but does not apply impersonation to the current thread 
    ''' principal
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnImpersonateWOthreads_Click(sender As System.Object, e As 
    System.EventArgsHandles btnImpersonateWOthreads.Click

        Try
            IPers = Impersonate.ImpersonateValidUser(txtUserId.Text, txtDomain.Text, 
            txtPassword.Text)
            t1 = New Thread(AddressOf ThreadTask)
            t1.IsBackground = True
            t1.Start()
        Catch ex As Exception
            MessageBox.Show(ex.Message, "Impersonation Error")
        End 
Try

    End Sub

    ''' <summary>
    ''' Removes Impersonation by reverting back to logged in user but does not
    ''' revert the thread principal back to the logged in user
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnUndoImpersonation_Click(ByVal sender As System.ObjectByVal e As 
    System.EventArgsHandles btnUndoImpersonation.Click

        If Not IPers Is Nothing Then
            ' undo impersonation at the level of the current windows identity
            IPers.Undo()
        End 
If

    End Sub

    ''' <summary>
    ''' Removes impersonation by reverting back to the logged in user and also 
    ''' reverts the thread principal back to the logged in user
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnUndoThreadImpersonation_Click(sender As System.Object, e As 
    System.EventArgsHandles btnUndoThreadImpersonation.Click

        If Not IPers Is Nothing Then
            ' undo impersonation at the level of the current windows identity
            IPers.Undo()

            ' removes prior thread impersonation by setting it back to the 
            ' current windows identity
            Impersonate.RestoreThreadPrincipal()
        End 
If

    End Sub

    ''' <summary>
    
''' Inform on the current windows identity and the current thread principal identity 
    ''' - confirms that the 
    ''' threads are running under the impersonated identity or not
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub btnWhoIsUser_Click(ByVal sender As System.ObjectByVal e As 
    System.EventArgsHandles btnWhoIsUser.Click

        Dim sb As New StringBuilder
        sb.Append("Current Principal:  " & 
        System.Security.Principal.WindowsIdentity.GetCurrent().Name().ToString() & 
        Environment.NewLine)
        sb.Append("Current Thread Principal:  " & 
        Thread.CurrentPrincipal.Identity.Name().ToString())

        MessageBox.Show(sb.ToString(), "Current Identities")

    End Sub

#End Region

#Region "Class Methods"

    ''' <summary>
    ''' Useless long running task - 
    ''' This just provides a repeating task that can run under the thread
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub ThreadTask()

        Dim counter As Integer = 0
        Dim endVal As Integer = 3

        ' unending loop to represent long running task
        Do Until counter = endVal
            If counter = (endVal - 1) Then
                counter = 0
            Else
                counter += 1
            End If
        Loop
    End 
Sub

#End Region

End Class

Summary

The article addresses a simple approach that may be used to extend impersonation to include threading; by default, establishing an impersonation context has no effect upon the account under which threads are spawned. However, with a little bit of extra efforts it is not difficult to include threads within the impersonation. Depending upon what the threads are doing and accessing, it may be essential to enforce the impersonation at the level of the threads