Search code examples
.netwindowswcfnamed-pipesimpersonation

How to call net.pipe (named pipe) WCF services while impersonating in a Windows Service


I am having an issue calling a WCF service over net.pipe with Windows impersonation from a C# Windows service.

Background

The service reads from a queue and creates children app domains, each running a particular module per the item pulled from the queue. We call the Windows service a “JobQueueAgent” and each module a “Job”. I will use these terms going forward. A job can be configured to run as a specified user. We use impersonation inside the job’s app domain to accomplish this. The following is the flow of logic and credentials in the service:

JobQueueAgent (Windows Service – Primary User) >> Create job domain >> Job Domain (App Domain) >> Impersonate sub user >> Run job on thread with impersonation >> Job (Module – Sub User) >> Job logic

The “Primary User” and “Sub User” are both domain accounts with rights to “login as a service”.

The service runs on a virtual server running Windows Server 2012 R2.

The following is the C# impersonation code I am using:

namespace JobQueue.WindowsServices
{
    using System;
    using System.ComponentModel;
    using System.Net;
    using System.Runtime.InteropServices;
    using System.Security.Authentication;
    using System.Security.Permissions;
    using System.Security.Principal;
    internal sealed class ImpersonatedIdentity : IDisposable
    {
        [PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
        public ImpersonatedIdentity(NetworkCredential credential)
        {
            if (credential == null) throw new ArgumentNullException("credential");

            if (LogonUser(credential.UserName, credential.Domain, credential.Password, 5, 0, out _handle))
            {
                _context = WindowsIdentity.Impersonate(_handle);
            }
            else
            {
                throw new AuthenticationException("Impersonation failed.", newWin32Exception(Marshal.GetLastWin32Error()));
            }
        }
        ~ImpersonatedIdentity()
        {
            Dispose();
        }
        public void Dispose()
        {
            if (_handle != IntPtr.Zero)
            {
                CloseHandle(_handle);
                _handle = IntPtr.Zero;
            }
            if (_context != null)
            {
                _context.Undo();
                _context.Dispose();
                _context = null;
            }
            GC.SuppressFinalize(this);
        }
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool LogonUser(string userName, string domain, string password, int logonType,int logonProvider, out IntPtr handle);

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr handle);
        private IntPtr _handle = IntPtr.Zero;
        private WindowsImpersonationContext _context;
    }
}

The Problem

Some jobs are required to make net.pipe WCF service calls to another Windows service running on the server. The net.pipe call fails when running under impersonation.

Here is the exception I get in this scenario:

Unhandled Exception: System.ComponentModel.Win32Exception: Access is denied

Server stack trace: at System.ServiceModel.Channels.AppContainerInfo.GetCurrentProcessToken() at System.ServiceModel.Channels.AppContainerInfo.RunningInAppContainer() at System.ServiceModel.Channels.AppContainerInfo.get_IsRunningInAppContainer() at System.ServiceModel.Channels.PipeSharedMemory.BuildPipeName(String pipeGuid)

The net.pipe succeeds when not running under impersonation. The net.pipe call also succeeds when the impersonated user is added to the Administrators group. This implies there is some privilege the user needs to make the call while under impersonation. We have not been able to determine what policy, privilege or access the user needs to make the net.pipe call while impersonating. It is not acceptable to make the user an administrator.

Is this a known issue? Is there a particular right the user needs to succeed? Is there a code change I can make to resolve this issue? Using WCF's net.pipe in a website with impersonate=true seems to indicate that this will not work in an ASP.NET application due to NetworkService. Not sure, but that shouldn’t apply here.


Solution

  • With the help of Microsoft Support, I was able to resolve this issue by modifying the access rights of the thread identity (something suggested by Harry Johnston in another answer). Here is the impersonation code I am now using:

    using System;
    using System.ComponentModel;
    using System.Net;
    using System.Runtime.InteropServices;
    using System.Security.AccessControl;
    using System.Security.Authentication;
    using System.Security.Permissions;
    using System.Security.Principal;
    
    internal sealed class ImpersonatedIdentity : IDisposable
    {
        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        public ImpersonatedIdentity(NetworkCredential credential)
        {
            if (credential == null) throw new ArgumentNullException(nameof(credential));
    
            _processIdentity = WindowsIdentity.GetCurrent();
    
            var tokenSecurity = new TokenSecurity(new SafeTokenHandleRef(_processIdentity.Token), AccessControlSections.Access);
    
            if (!LogonUser(credential.UserName, credential.Domain, credential.Password, 5, 0, out _token))
            {
                throw new AuthenticationException("Impersonation failed.", new Win32Exception(Marshal.GetLastWin32Error()));
            }
    
            _threadIdentity = new WindowsIdentity(_token);
    
            tokenSecurity.AddAccessRule(new AccessRule<TokenRights>(_threadIdentity.User, TokenRights.TOKEN_QUERY, InheritanceFlags.None, PropagationFlags.None, AccessControlType.Allow));
            tokenSecurity.ApplyChanges();
    
            _context = _threadIdentity.Impersonate();
        }
    
        ~ImpersonatedIdentity()
        {
            Dispose();
        }
    
        public void Dispose()
        {
            if (_processIdentity != null)
            {
                _processIdentity.Dispose();
                _processIdentity = null;
            }
            if (_token != IntPtr.Zero)
            {
                CloseHandle(_token);
                _token = IntPtr.Zero;
            }
            if (_context != null)
            {
                _context.Undo();
                _context.Dispose();
                _context = null;
            }
    
            GC.SuppressFinalize(this);
        }
    
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool LogonUser(string userName, string domain, string password, int logonType, int logonProvider, out IntPtr handle);
    
        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr handle);
    
        private WindowsIdentity _processIdentity;
        private WindowsIdentity _threadIdentity;
        private IntPtr _token = IntPtr.Zero;
        private WindowsImpersonationContext _context;
    
    
        [Flags]
        private enum TokenRights
        {
            TOKEN_QUERY = 8
        }
    
    
        private class TokenSecurity : ObjectSecurity<TokenRights>
        {
            public TokenSecurity(SafeHandle safeHandle, AccessControlSections includeSections)
                : base(false, ResourceType.KernelObject, safeHandle, includeSections)
            {
                _safeHandle = safeHandle;
            }
    
            public void ApplyChanges()
            {
                Persist(_safeHandle);
            }
    
            private readonly SafeHandle _safeHandle;
        }
    
        private class SafeTokenHandleRef : SafeHandle
        {
            public SafeTokenHandleRef(IntPtr handle)
                : base(IntPtr.Zero, false)
            {
                SetHandle(handle);
            }
    
            public override bool IsInvalid
            {
                get { return handle == IntPtr.Zero || handle == new IntPtr(-1); }
            }
            protected override bool ReleaseHandle()
            {
                throw new NotImplementedException();
            }
        }
    }