Yet on BizTalk Impersonation With WCF Adapters

19.10.2018

Scenario

Last year I wrote a post on how using BizTalk Server 2006 R2/2009 and Protocol Transition to impersonate the original caller when invoking a downstream service that uses the Windows Integrated Security. Recently, one customer posed the following question to my colleague, Tim Wieman:

Can I create a WCF Send Port that is able to impersonate a statically defined domain user, other than then the service account of the host instance process, when calling a downstream WCF service that exposes a BasicHttpBinding/WsHttpBinding endpoint configured to use the Transport security mode ?

The answer is:

  • Yes, when using the Basic or the Digest authentication scheme.
  • No, when using the Windows or NTLM client credential type.

To verify this constraint, you can proceed as follows:

  • Open the BizTalk Administration Console.
  • Create a new WCF-BasicHttp or WCF-WsHttp Static Solicit-Response Send Port
  • Click the Configure button.
  • Select the Security tab.
  • Choose the Transport security mode.

At this point, if you select the Basic or Digest transport client credential type from the corresponding drop-down list:

  • You can click the Edit button in the User name credentials section, as highlighted in the picture below.
  • You can specify the Username and Password of the account that the Send Port will impersonate when invoking the target WCF service. As an alternative, you can leverage the Single Sign-On to redeem a ticket and pick a user at runtime from a certain affiliate application.

Instead, if you select the Ntlm or Windows transport client credential type from the corresponding drop-down list, the Edit button in the User name credentials section is greyed out, as shown in the picture below:

So at this point some of you might ask yourselves:

How can I impersonate a statically-defined user, different from the service account of the host process running my WCF Send Port, when invoking an underlying WCF service that uses the Transport security mode along with the Ntlm or Windows authentication scheme?

The answer is straightforward, you can achieve this objective using the WCF-Custom adapter and writing a custom WCF channel. Indeed, I didn't create a new component from scratch, I just used grabbed some code from MSDN, and extended the component I wrote one year ago for my previous post on BizTalk and Protocol Transition . In particular, I made the following changes:

  • I extended the following components:
    • InspectingBindingExtensionElement
    • InspectingBindingElement
    • InspectingChannelFactory
    • InspectingRequestChannel
  • to expose two additional properties:
    • WindowsUserName: gets or sets the domain account in the form of DOMAIN\Username that the Send Port will impersonate at runtime.
    • WindowsUserPassword: gets or sets the password of the domain account.
  • Then I extended the WindowsUserPositionEnum type and therefore the WindowsUserPosition property to include a new mode called Static. As a consequence, the custom channel at runtime will retrieve the client credentials in a different way depending on the value of the WindowsUserPosition property exposed by the InspectingBindingExtensionElement component:
    • Context: username will be read from the message context property identified by the ContextPropertyName and ContextPropertyNamespace properties exposed by the InspectingBindingExtensionElement. In this case, the environment must be properly configured to use Protocol Transition. See my previous post for more details.
    • Message: username will be read from the message using the XPath expression contained in the WindowsUserXPath property. Even in this case, the environment must be properly configured to use Protocol Transition. See my previous post for more details.
    • Static: the custom channel will use the client credentials contained in the WindowsUserName and WindowsUserPassword to impersonate the corresponding domain account before invoking the downstream WCF service. Note: this pattern requires the component to uses the client credentials to invoke the LogonUser function at runtime, but it does not require to configure the BizTalk environment for Protocol Transition.
  • I finally extended the InspectingRequestChannel and InspectingHelper classes to support the new Static mode. In particular, the custom channel at runtime performs the following steps to impersonate a given domain account declaratively defined in the WCF Send Port configuration:
    • It calls the LogonUser static method exposed by the InspectingHelper class which in turn invokes the LogonUser Windows function which returns a token handle.
    • The channel creates a new WindowsIdentity object using the constructor that accepts the user token returned by the previous call.
    • Then, it invokes the Impersonate method exposed by the WindowsIdentity object.
    • The channel invokes the underlying channel.
    • In the finally block, when the call is complete, the channel invokes the Undo method on the WindowsImpersonationContext object returned by the Impersonate method, an then it invokes the CloseHandle static method exposed by the InspectingHelper class which in turn invokes the CloseHandle Windows function.

For your convenience, I report below the new code for the InspectingRequestChannel and InspectingHelper classes (I purposely omitted parts for ease of reading):

InspectingHelper class

#region Copyright //------------------------------------------------- // Author: Paolo Salvatori // Email: paolos@microsoft.com // History: 2008-09-17 Created //------------------------------------------------- #endregion #region Using Directives using System; using System.Diagnostics; using System.Configuration; using System.Runtime.InteropServices; using System.Security.Principal; using System.Security.Permissions; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Configuration; using System.DirectoryServices.ActiveDirectory; using System.Xml; using System.IO; using System.Text; using Microsoft.BizTalk.XPath; #endregion namespace Microsoft.BizTalk.CAT.Samples.ProtocolTransition.WCFExtensionLibrary { /// <summary> /// This class exposes the logic to impersonate another user using the Protocol Transition mechanism. /// </summary> public class InspectingHelper { #region DllImport [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken); [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public extern static bool CloseHandle(IntPtr handle); #endregion #region Private Constants ...
// The following constants are used when calling the LogonUser external function private const int LOGON32_PROVIDER_DEFAULT = 0; //This parameter causes LogonUser to create a primary token. private const int LOGON32_LOGON_INTERACTIVE = 2; #endregion #region Private Static Fields private static string domainFQDN = string.Empty; #endregion #region Public Static Constructor static InspectingHelper() { try { Domain domain = Domain.GetComputerDomain(); if (domain != null) { domainFQDN = domain.Name; } } catch (Exception ex) { Debug.WriteLine(string.Format(MessageFormat, ex.Message)); } } #endregion #region Static Public Methods public static string GetUserPrincipalName(ref Message message, WindowsUserPositionEnum windowsUserPosition, string contextPropertyName, string contextPropertyNamespace, string windowsUserXPath, string windowsUserName, string windowsUserPassword, int maxBufferSize, bool traceEnabled, out string userName, out string domainName) { string windowsUser = null; domainName = null; userName = null; try { switch (windowsUserPosition) { case WindowsUserPositionEnum.Message: if (message != null && !string.IsNullOrEmpty(windowsUserXPath)) { MessageBuffer messageBuffer = message.CreateBufferedCopy(maxBufferSize); if (messageBuffer == null) { throw new ApplicationException(MessageBufferCannotBeNull); } Message clone = messageBuffer.CreateMessage(); if (message == null) { throw new ApplicationException(CloneCannotBeNull); } message = messageBuffer.CreateMessage(); if (message == null) { throw new ApplicationException(MessageCannotBeNull); } XmlDictionaryReader xmlDictionaryReader = clone.GetReaderAtBodyContents(); if (xmlDictionaryReader == null) { throw new ApplicationException(XmlDictionaryReaderCannotBeNull); } XPathCollection xPathCollection = new XPathCollection(); if (xPathCollection == null) { throw new ApplicationException(XPathCollectionCannotBeNull); } XPathReader xPathReader = new XPathReader(xmlDictionaryReader, xPathCollection); if (xPathReader == null) { throw new ApplicationException(XPathReaderCannotBeNull); } xPathCollection.Add(windowsUserXPath); bool ok = false; while (xPathReader.ReadUntilMatch()) { if (xPathReader.Match(0) && !ok) { windowsUser = xPathReader.ReadString(); ok = true; } } } break; case WindowsUserPositionEnum.Context: if (string.IsNullOrEmpty(contextPropertyName)) { throw new ApplicationException(ContextPropertyNameCannotBeNull); } if (string.IsNullOrEmpty(contextPropertyNamespace)) { throw new ApplicationException(ContextPropertyNamespaceCannotBeNull); } string contextPropertyKey = string.Format(ContextPropertyKeyFormat,
contextPropertyNamespace,
contextPropertyName); if (message.Properties.ContainsKey(contextPropertyKey)) { windowsUser = message.Properties[contextPropertyKey] as string; } else { throw new ApplicationException(string.Format(NoContextPropertyFormat, contextPropertyKey)); } break; case WindowsUserPositionEnum.Static: windowsUser = windowsUserName; break; } if (!string.IsNullOrEmpty(windowsUser)) { string[] parts = windowsUser.Split(new char[] { Path.DirectorySeparatorChar }); if (parts != null && parts.Length > 1) { domainName = parts[0]; userName = parts[1]; Debug.WriteLineIf(traceEnabled, string.Format(CreatingUPNFormat, windowsUser)); string upn = string.Format(UserPrincipalNameFormat, parts[1], domainFQDN); Debug.WriteLineIf(traceEnabled, string.Format(UsingUserPrincipalNameFormat, upn)); return upn; } } } catch (Exception ex) { Debug.WriteLineIf(traceEnabled, string.Format(MessageFormat, ex.Message)); throw ex; } return null; } public static bool LogonUser(string userName, string domainName, string windowsUserPassword, bool traceEnabled, ref IntPtr tokenHandle) { Debug.WriteLineIf(traceEnabled, string.Format(StartLogonUserFormat,
domainName ?? Unknown,
userName ?? Unknown)); // Call LogonUser to obtain a handle to an access token. bool ok = LogonUser(userName, domainName, windowsUserPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref tokenHandle); if (traceEnabled) { if (ok) { Debug.WriteLineIf(traceEnabled, string.Format(LogonUserSucceededFormat,
domainName ?? Unknown,
userName ?? Unknown)); } else { Debug.WriteLineIf(traceEnabled, string.Format(LogonUserFailedFormat,
domainName ?? Unknown,
userName ?? Unknown)); } } return ok; } public static bool CloseHandle(IntPtr tokenHandle, string userName, string domainName, bool traceEnabled) { Debug.WriteLineIf(traceEnabled, string.Format(StartCloseTokenFormat,
domainName ?? Unknown,
userName ?? Unknown)); bool ok = CloseHandle(tokenHandle); if (traceEnabled) { if (ok) { Debug.WriteLineIf(traceEnabled, string.Format(CloseTokenSucceededFormat,
domainName ?? Unknown,
userName ?? Unknown)); } else { Debug.WriteLineIf(traceEnabled, string.Format(CloseTokenFailedFormat,
domainName ?? Unknown,
userName ?? Unknown)); } } return ok; } #endregion } }

InspectingRequestChannel class

public class InspectingRequestChannel : InspectingChannelBase<IRequestChannel>, IRequestChannel { ... public Message Request(Message message, TimeSpan timeout) { Message reply = null; string upn = null; WindowsImpersonationContext impersonationContext = null; IntPtr tokenHandle = new IntPtr(0); string userName = null; string domainName = null; try { if (componentEnabled) { WindowsIdentity identity = null; upn = InspectingHelper.GetUserPrincipalName(ref message, windowsUserPosition, contextPropertyName, contextPropertyNamespace, windowsUserXPath, windowsUserName, windowsUserPassword, maxBufferSize, traceEnabled, out userName, out domainName); if (windowsUserPosition == WindowsUserPositionEnum.Static) { // Call LogonUser to obtain a handle to an access token. bool returnValue = InspectingHelper.LogonUser(userName, domainName, windowsUserPassword, traceEnabled, ref tokenHandle); // Protocol Transition is not necessary in this case identity = new WindowsIdentity(tokenHandle); } else { if (!string.IsNullOrEmpty(upn)) { // Protocol Transition must be properly configured, // otherwise the impersonation will fail identity = new WindowsIdentity(upn); } } Debug.WriteLineIf(traceEnabled, string.Format(ImpersonatingFormat, upn)); impersonationContext = identity.Impersonate(); Debug.WriteLineIf(traceEnabled, string.Format(ImpersonatedFormat, upn)); } Debug.WriteLineIf(traceEnabled, CallingWebService); reply = this.InnerChannel.Request(message); Debug.WriteLineIf(traceEnabled, WebServiceCalled); } catch (Exception ex) { Debug.WriteLineIf(traceEnabled, string.Format(MessageFormat, ex.Message)); throw ex; } finally { if (impersonationContext != null) { impersonationContext.Undo(); Debug.WriteLineIf(traceEnabled, string.Format(ImpersonationUndoneFormat, upn ?? Unknown)); } if (tokenHandle != IntPtr.Zero) { InspectingHelper.CloseHandle(tokenHandle, userName, domainName, traceEnabled); } } return reply; } }

Test Case

To test my component, I created the following test case:

WinForm driver application submits a new request to a WCF-NetTcp Request-Response Receive Location

  1. The Message Agent submits the incoming request message to the MessageBox (BizTalkMsgBoxDb).
  2. The inbound request is consumed by a Solicit Response WCF-Custom Send Port. This latter uses a Filter Expression to receive all the documents published by the Receive Port hosting the WCF Receive Location.
  3. The inbound message is mapped to the request format by the downstream HelloWorldService web service. This latter is hosted by IIS and exposes a single WsHttpBinding endpoint.
  4. The WCF-Custom Send Port impersonates the user statically defined in the Port configuration and invokes the underlying HelloWorldService WCF service that in the scenario is hosted by a separate Application Pool (w3wp.exe) on the same IIS instance.
  5. The HelloWorldService WCF service returns a response message.
  6. The incoming response message is mapped to the format expected by the client application.
  7. The transformed response message is published to the MessageBox.
  8. The response message is retrieved by the Request-Response WCF Custom Receive Location which originally received the request call.
  9. The response message is returned to the client WinForm application.

The following picture shows the binding configuration of the WCF Send Port used to communicate with the HelloWorldService.

Finally, the picture below reports the trace captured during a test run.