On a recent project I needed to resolve the identity of clients calling an orchestration exposed as a WCF service. Clients would use a X.509 certificate to sign the message. Configuring the WCF service was easy enough but I was not getting the party resolution piece working correctly. The WCF adapter (I was using WCF-CustomIsolated) was not populating the context property (BTS.SignatureCertificate) that the party resolution component uses to lookup the party even though the client certificate was being validated. The WCF adapter was dumping the soap headers into the context. I was left either to parse the headers manually and find a way to grab details of the signing certificate or somehow get the WCF adapter to do this work for me (as it was already validating the client certificate and checking we had the corresponding public key in the certificate store). Fortunately, I found a way we can get the adapter to help out.
The solution was to create a WCF service behavior extension to intercept message processing by the adapter (note this takes place before the message is presented to the receive pipeline). The custom behavior looks for a client certificate and if found writes the thumbprint into a custom soap header. The WCF Adapter would then write my custom header into the message context and I could grab it in a custom pipeline component. I chose to write a component to execute before the OOTB party resolution component and populate the BTS.SignatureCertificate context property with the value of certificate thumbprint. I could of done this all in one component and performed custom party resolution but thought this might be a bit cleaner.
So looking at the WCF service behavior
1: using System;
2: using System.ServiceModel;
3: using System.ServiceModel.Dispatcher;
4: using System.ServiceModel.Channels;
5: using System.ServiceModel.Description;
6: using System.IdentityModel.Claims;
7: using System.ServiceModel.Configuration;
8:
9: namespace Breeze.WCF.ClientCertificateContext
10: { 11: public class MessageInspector : IDispatchMessageInspector, IServiceBehavior
12: { 13: #region IDispatchMessageInspector Members
14:
15: object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
16: { 17: object correlationState = null;
18: string thumbprint = "";
19:
20: try
21: { 22: // Gather thumbprint of signing certificate used by the client
23: foreach (ClaimSet set in request.Properties.Security.ServiceSecurityContext.AuthorizationContext.ClaimSets)
24: { 25: foreach (Claim claim in set.FindClaims(ClaimTypes.Thumbprint, Rights.Identity))
26: { 27: thumbprint = BitConverter.ToString((byte[])claim.Resource);
28: thumbprint = thumbprint.Replace("-", ""); 29: }
30: }
31:
32: // Write this away as a custom message header
33: if (!String.IsNullOrEmpty(thumbprint))
34: { 35: MessageHeader header = MessageHeader.CreateHeader("ClientCertificate", "http://schemas.breeze.net/BizTalk/WCF-properties", thumbprint); 36: request.Headers.Add(header);
37: }
38:
39: }
40: catch (Exception ex)
41: { 42: System.Diagnostics.EventLog.WriteEntry("WCF MessageInspector", String.Format("Exception caught: {0}", ex.ToString())); 43: }
44:
45: return correlationState;
46: }
47:
48: void IDispatchMessageInspector.BeforeSendReply(ref Message reply, object correlationState)
49: { 50: }
51:
52: #endregion
53:
54: #region IServiceBehavior Members
55:
56: public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase,
System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
57: { 58: return;
59: }
60:
61: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
62: { 63: foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
64: { 65: foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
66: { 67: endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
68: }
69: }
70: }
71:
72: public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
73: { 74: return;
75: }
76:
77: #endregion
78: }
79:
80: public class MessageInspectorElement : BehaviorExtensionElement
81: { 82: public override Type BehaviorType
83: { 84: get { return typeof(MessageInspector); } 85: }
86:
87: protected override object CreateBehavior()
88: { 89: return new MessageInspector();
90: }
91: }
92:
93:
94: }
Tip: Don't forget to implement the BehaviorExtensionElement. You’ll need this to apply the service behavior via configuration (in the receive location) rather then having to do it programmatically. You will also need to sign, GAC and register the service behavior extension element in the machine.config (or service’s web.config in IIS)
With the WCF service behavior bits done, we need to add it to our receive location:
If you were to test the solution now, you’ll get the thumbprint of the client certificate written to your custom context property (http://schemas.breeze.net/BizTalk/WCF-properties#ClientCertificate) and will look something like this:
1: <ClientCertificate xmlns="http://schemas.breeze.net/BizTalk/WCF-properties">11C3E164C41ADC8DBA0EA6558784B9FAE19E398D</ClientCertificate>
I had thought I might be able to get away with writing this directly into the BTS.SignatureCertificate context property but the format is clearly different. The BTS.SignatureCertificate property needs just the certificate thumbprint string and obviously we have the xml wrapper. So we must create a simple pipeline component to sit somewhere before the party resolution component to grab the certificate thumbprint out of our custom context property and write it into the context property the party resolver component is looking for.
After deploying and setting the receive pipeline to use the custom one above, I got party resolution working like a bought one with the BTS.SigningCertificate, BTS.SourcePartyID and MessageTracking.PartyName context properties populated.
I guess I was a little surprised that all this was needed. WCF does a great job of abstracting out all the transport and security bits and moving them to configuration time (no additional code in our service or client). In the HTTP and SOAP adapter days, the MIME/SMIME pipeline component was used to decrypt and validate the signing certificate as well as populating the required context properties. Why doesn’t the WCF Adapter perform this part in the same way? I mean, its doing the decoding, decrypting and certificate validation. So why not the populating of these context properties? Perhaps there is secret squirrel checkbox somewhere I missed. Love to hear comments if anyone has done this differently?
[Updated: 19-10-2010]
Thanks to Thiago (see comments section) we have been able to simplify this further. The WCF adapter provides some “special” namespaces that allow us to instruct the adapter to write context properties in a more controlled way. Specifically we can instruct the adapter to write directly into defined property schema elements (e.g. OOTB BizTalk property schemas or deployed custom property schemas). This allows us to write the certificate thumbprint directly into the BTS.SigningCertificate context property and avoid the need for the custom pipeline component to move the value from the custom header property into the BTS.SigningCertificate property as described above.
To do this we simply change the IDispatchMessageInspector.AfterReceiveRequest to make use of these special namespaces.
// Write this away as a custom message header
if (!String.IsNullOrEmpty(thumbprint))
{ // Write the thumbprint directly to the BTS.SigningCertificate context property
// Thanks to Thiago http://connectedthoughts.wordpress.com
// Create a collection of context properties we want the adapter to write/promote for us
XmlQualifiedName clientCertificateProp =
new XmlQualifiedName("SignatureCertificate", "http://schemas.microsoft.com/BizTalk/2003/system-properties"); //Maps to BTS.SignatureCertificate List<KeyValuePair<XmlQualifiedName, object>> promoteProps = new List<KeyValuePair<XmlQualifiedName, object>>();
promoteProps.Add(new KeyValuePair<XmlQualifiedName, object>(clientCertificateProp, thumbprint));
// Add the property collection to the request
// Use the http://..../Promote to have the adapter promote the context prop
// or use http:/...../WriteToContext to just have the property written but not promoted.
request.Properties.Add("http://schemas.microsoft.com/BizTalk/2006/01/Adapters/WCF-properties/Promote", promoteProps); }
Now we can do away with the custom pipeline component bits and use the OOTB XMLReceive pipeline (as it contains the party resolver component already). The certificate thumbprint will be written directly into the BTS.SigningCertificate context property (and promoted) ready for the party resolver component to use.
Nice work Thiago. 