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:
To verify this constraint, you can proceed as follows:
At this point, if you select the Basic or Digest transport client credential type from the corresponding drop-down list:
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:
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; } }
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
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.
As I explained in one of my recent posts, using WCF extensibility points allows you customize in-depth the default behavior of BizTalk WCF Adapters. In particular, the WCF-Custom Adapter provides the possibility to specify the customize the composition of the binding and hence of the channel stack that will be created and used at runtime to communicate with external applications.
In this article we have seen how to exploit this characteristic to workaround and bypass a constraint of WCF Adapters. As usual, I had just a few hours to write the code and write the article, so should you find an error or a problem in my component, please send me an email or leave a comment on my blog, thanks!
You can find the new version of the code here.