Reputation: 1930
I am implementing an API from a bank and they require a security token to be provided. In the header of every soap message there is something which looks as follows:
<soapenv:Header>
<tpw:BinarySecurityToken ValueType="MAC" Id="DesMacToken" EncodingType="Base64" Value="**xvz**"/>
</soapenv:Header>
According to their documentation I need to generate an 8 byte MAC value on the body of each message. The MAC is generated by the CBC-MAC algorithm and DES as the block cipher. The contents of the soapenv:Body tag of each message is used as the data for the MAC calculation.
So my question is how do I get WCF to do this? I have put the following code together to create the MAC value, but am unsure how to get this into the header of every message.
private string GenerateMAC(string SoapXML)
{
ASCIIEncoding encoding = new ASCIIEncoding();
//Convert from Hex to Bin
byte[] Key = StringToByteArray(HexKey);
//Convert String to Bytes
byte[] XML = encoding.GetBytes(SoapXML);
//Perform the Mac goodies
MACTripleDES DesMac = new MACTripleDES(Key);
byte[] Mac = DesMac.ComputeHash(XML);
//Base64 the Mac
string Base64Mac = Convert.ToBase64String(Mac);
return Base64Mac;
}
public static byte[] StringToByteArray(string Hex)
{
if (Hex.Length % 2 != 0)
{
throw new ArgumentException();
}
byte[] HexAsBin = new byte[Hex.Length / 2];
for (int index = 0; index < HexAsBin.Length; index++)
{
string bytevalue = Hex.Substring(index * 2, 2);
HexAsBin[index] = Convert.ToByte(bytevalue, 16);
}
return HexAsBin;
}
Any help will be greatly appreciated.
More info: The bank has provided a WSDL which I have used as a service reference. An example of a response that is sent:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
[System.ServiceModel.MessageContractAttribute(WrapperName="LogonRequest", WrapperNamespace="http://webservice.com", IsWrapped=true)]
public partial class LogonRequest {
[System.ServiceModel.MessageHeaderAttribute(Namespace="http://webservice.com")]
public DataAccess.BankService.BinarySecurityToken BinarySecurityToken;
The BinarySecurityToken (that goes in the header) looks as follows:
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.0.30319.233")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="http://webservice.com")]
public partial class BinarySecurityToken : object, System.ComponentModel.INotifyPropertyChanged {
private string valueTypeField;
private string idField;
private string encodingTypeField;
private string valueField;
public BinarySecurityToken() {
this.valueTypeField = "MAC";
this.idField = "DesMacToken";
this.encodingTypeField = "Base64";
}
Upvotes: 4
Views: 4397
Reputation: 1930
DavidJones had the right answer, but I wanted to post my class in case anyone else needs to do something similar:
public class SoapHeaderBehaviour : BehaviorExtensionElement, IClientMessageInspector, IEndpointBehavior
{
public void AfterReceiveReply(ref Message reply, object correlationState)
{
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
try
{
// Get the request into an XDocument
var memoryStream = new MemoryStream();
var writer = XmlDictionaryWriter.CreateTextWriter(memoryStream);
request.WriteMessage(writer);
writer.Flush();
memoryStream.Position = 0;
var xmlDoc = new XmlDocument();
xmlDoc.Load(memoryStream);
// get the body tag
XmlNode bodyNode = FindNode("body", xmlDoc);
Debug.Assert(bodyNode != null, "Unable to find the BODY in the SOAP message");
if (bodyNode != null)
{
string MAC = GenerateMAC(bodyNode.InnerXml);
// replace the relevant item in the header
XmlNode tokenNode = FindNode("binarysecuritytoken", xmlDoc);
Debug.Assert(tokenNode != null, "Unable to find the BinarySecurityToken in the SOAP message");
if (tokenNode != null)
{
tokenNode.Attributes["Value"].Value = MAC;
// recreate the request
memoryStream = new MemoryStream();
writer = XmlDictionaryWriter.CreateTextWriter(memoryStream);
xmlDoc.WriteTo(writer);
writer.Flush();
memoryStream.Position = 0;
var reader = XmlDictionaryReader.CreateTextReader(memoryStream, XmlDictionaryReaderQuotas.Max);
var newRequest = Message.CreateMessage(reader, int.MaxValue, request.Version);
request = newRequest;
}
}
}
catch (Exception ex)
{
}
return null;
}
private XmlNode FindNode(string name, XmlDocument xmlDoc)
{
XmlNode node = null;
for (int i = 0; i < xmlDoc.ChildNodes.Count; i++)
{
node = FindNode(name, xmlDoc.ChildNodes[i]);
if (node != null)
break;
}
return node;
}
private XmlNode FindNode(string name, XmlNode parentNode)
{
if (parentNode != null && parentNode.Name.ToLower().Contains(name))
{
return parentNode;
}
XmlNode childNode = null;
for (int i = 0; i < parentNode.ChildNodes.Count; i++)
{
childNode = FindNode(name, parentNode.ChildNodes[i]);
if (childNode != null)
break;
}
return childNode;
}
private string GenerateMAC(string soapXML)
{
// get the key from the web.config file
var key = ConfigurationManager.AppSettings["Key"];
ASCIIEncoding encoding = new ASCIIEncoding();
//Convert from Hex to Bin
byte[] keyBytes = StringToByteArray(key);
//Convert String to Bytes
byte[] xmlBytes = encoding.GetBytes(soapXML);
//Perform the Mac goodies
MACTripleDES desMac = new MACTripleDES(keyBytes);
byte[] macBytes = desMac.ComputeHash(xmlBytes);
//Base64 the Mac
string base64Mac = Convert.ToBase64String(macBytes);
return base64Mac;
}
private static byte[] StringToByteArray(string hex)
{
if (hex.Length % 2 != 0)
{
throw new ArgumentException();
}
byte[] hexBytes = new byte[hex.Length / 2];
for (int index = 0; index < hexBytes.Length; index++)
{
string bytevalue = hex.Substring(index * 2, 2);
hexBytes[index] = Convert.ToByte(bytevalue, 16);
}
return hexBytes;
}
protected override object CreateBehavior()
{
return new SoapHeaderBehaviour();
}
public override Type BehaviorType
{
get
{
return GetType();
}
}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime behavior)
{
behavior.MessageInspectors.Add(this);
// behavior.MessageInspectors.Add(new FaultMessageInspector());
}
public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint, EndpointDispatcher endpointDispatcher)
{
}
public void Validate(ServiceEndpoint serviceEndpoint)
{
}
}
Upvotes: 0
Reputation: 252
I had to do something like this recently and what I ended up doing was creating a behaviour that implemented IClientMessageInspector
and used the BeforeSendRequest
method to create data for my header and then populate it into the SOAP request.
public class SoapHeaderBehaviour : BehaviorExtensionElement, IClientMessageInspector
{
public void AfterReceiveReply(ref Message reply, object correlationState) { }
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
var security = new Security(); // details irrelevant
var messageHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", security, new ConcreteXmlObjectSerializer(typeof(Security)), true);
request.Headers.Add(messageHeader);
return null;
}
protected override object CreateBehavior() { return new SoapHeaderBehaviour(); }
public override Type BehaviorType { get { return GetType(); } }
}
ConcreteXmlObjectSerializer is a class I found on the internet somewhere (unfortunately can't seem to find it right now) that just worked. Here is the code for that:
public class ConcreteXmlObjectSerializer : XmlObjectSerializer
{
readonly Type objectType;
XmlSerializer serializer;
public ConcreteXmlObjectSerializer(Type objectType)
: this(objectType, null, null)
{
}
public ConcreteXmlObjectSerializer(Type objectType, string wrapperName, string wrapperNamespace)
{
if (objectType == null)
throw new ArgumentNullException("objectType");
if ((wrapperName == null) != (wrapperNamespace == null))
throw new ArgumentException("wrapperName and wrapperNamespace must be either both null or both non-null.");
if (wrapperName == string.Empty)
throw new ArgumentException("Cannot be the empty string.", "wrapperName");
this.objectType = objectType;
if (wrapperName != null)
{
XmlRootAttribute root = new XmlRootAttribute(wrapperName);
root.Namespace = wrapperNamespace;
this.serializer = new XmlSerializer(objectType, root);
}
else
this.serializer = new XmlSerializer(objectType);
}
public override bool IsStartObject(XmlDictionaryReader reader)
{
throw new NotImplementedException();
}
public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName)
{
Debug.Assert(serializer != null);
if (reader == null) throw new ArgumentNullException("reader");
if (!verifyObjectName)
throw new NotSupportedException();
return serializer.Deserialize(reader);
}
public override void WriteStartObject(XmlDictionaryWriter writer, object graph)
{
throw new NotImplementedException();
}
public override void WriteObjectContent(XmlDictionaryWriter writer, object graph)
{
if (writer == null) throw new ArgumentNullException("writer");
if (writer.WriteState != WriteState.Element)
throw new SerializationException(string.Format("WriteState '{0}' not valid. Caller must write start element before serializing in contentOnly mode.",
writer.WriteState));
using (MemoryStream memoryStream = new MemoryStream())
{
using (XmlDictionaryWriter bufferWriter = XmlDictionaryWriter.CreateTextWriter(memoryStream, Encoding.UTF8))
{
serializer.Serialize(bufferWriter, graph);
bufferWriter.Flush();
memoryStream.Position = 0;
using (XmlReader reader = new XmlTextReader(memoryStream))
{
reader.MoveToContent();
writer.WriteAttributes(reader, false);
if (reader.Read()) // move off start node (we want to skip it)
{
while (reader.NodeType != XmlNodeType.EndElement) // also skip end node.
writer.WriteNode(reader, false); // this will take us to the start of the next child node, or the end node.
reader.ReadEndElement(); // not necessary, but clean
}
}
}
}
}
public override void WriteEndObject(XmlDictionaryWriter writer)
{
throw new NotImplementedException();
}
public override void WriteObject(XmlDictionaryWriter writer, object graph)
{
Debug.Assert(serializer != null);
if (writer == null) throw new ArgumentNullException("writer");
serializer.Serialize(writer, graph);
}
}
This is then hooked into the WCF client endpoint via the config file in 3 steps (all under the system.serviceModel
node:
Register the extension
<extensions>
<behaviorExtensions>
<add name="ClientSoapHeaderAdderBehaviour"
type="MyNamespace.SoapHeaderBehaviour, MyAssembly, Version=My.Version, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
Create an endpoint behaviour using it
<behaviors>
<endpointBehaviors>
<behavior name="MyEndpointBehaviours">
<ClientSoapHeaderAdderBehaviour />
</behavior>
</endpointBehaviors>
</behaviors>
Attach your endpoint behaviour to your client endpoint
<client>
<endpoint address="blah" binding="basicHttpBinding"
bindingConfiguration="blah" contract="blah"
name="blah"
behaviorConfiguration="MyEndpointBehaviours"/>
</client>
Hope this helps you.
Upvotes: 6