Tuesday, August 3, 2010

Attaching OASIS Username Tokens headers in WCF Requests

It’s been a while since I wrote that I was going to post this solution. I’ve been very busy and a little lazy to post. Anyway, here is how to attach the security tokens to a WCF Request:
First of all, we need to intercept the Request before send it. So we make use of the WCF extensibility capabilities by implementing from IEndPointBehavior.
public class InspectorBehavior : IEndpointBehavior
{
public ClientInspector ClientInspector { get; set; }
public InspectorBehavior(ClientInspector clientInspector)            
{             
ClientInspector = clientInspector;             
}
public void Validate(ServiceEndpoint endpoint)            
{}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)            
{             
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)            
{}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)            
{             
if (this.ClientInspector == null) throw new InvalidOperationException("Caller must supply ClientInspector.");             
clientRuntime.MessageInspectors.Add(ClientInspector);             
}             
}

Basically what we are doing is attaching a IClientMessageInspector to the request workflow. We want to manipulate the message so we can inject a custom header, with a ClientMessageInspector we have control over the request message.

public class ClientInspector : IClientMessageInspector 
{ 
public MessageHeader[] Headers { get; set; }
public ClientInspector(params MessageHeader[] headers) 
{ 
Headers = headers; 
}
public object BeforeSendRequest(ref Message request, IClientChannel channel) 
{ 
if (Headers != null) 
{ 
for (int i = Headers.Length - 1; i >= 0; i--) 
request.Headers.Insert(0, Headers[i]); 
} 
return request; 
}
public void AfterReceiveReply(ref Message reply, object correlationState) 
{ 
} 
}

Now that we are can manipulate the headers of the request we need to be sure to have one header to represent the Security Header we want to include in the request.

For this purpose we extend from MessageHeader an create our custom one:

public class SecurityHeader : MessageHeader 
{ 
public string SystemUser { get; set; } 
public string SystemPassword { get; set; }
public SecurityHeader(string systemUser, string systemPassword) 
{ 
SystemUser = systemUser; 
SystemPassword = systemPassword; 
}
public override string Name 
{ 
get { return "Security"; } 
}
public override string Namespace 
{ 
get { return "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; } 
}
protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion) 
{ 
WriteHeader(writer); 
}
private void WriteHeader(XmlDictionaryWriter writer) 
{ 
var nonce = new byte[64]; 
RandomNumberGenerator.Create().GetBytes(nonce); 
string created = DateTime.Now.ToString("yyyy-MM-ddThh:mm:ss.msZ");
writer.WriteStartElement("wsse", "UsernameToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); 
writer.WriteXmlnsAttribute("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"); 
writer.WriteStartElement("wsse", "Username", null); 
writer.WriteString(SystemUser); 
writer.WriteEndElement();//End Username 
writer.WriteStartElement("wsse", "Password", null); 
writer.WriteAttributeString("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"); 
writer.WriteString(ComputePasswordDigest(SystemPassword, nonce, created)); 
writer.WriteEndElement();//End Password 
writer.WriteStartElement("wsse", "Nonce", null); 
writer.WriteAttributeString("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"); 
writer.WriteBase64(nonce, 0, nonce.Length); 
writer.WriteEndElement();//End Nonce 
writer.WriteStartElement("wsu", "Created", null); 
writer.WriteString(created); 
writer.WriteEndElement();//End Created 
writer.WriteEndElement();//End UsernameToken
writer.Flush(); 
}
private string ComputePasswordDigest(string secret, byte[] nonceInBytes, string created) 
{ 
byte[] createdInBytes = Encoding.UTF8.GetBytes(created); 
byte[] secretInBytes = Encoding.UTF8.GetBytes(secret);
byte[] concatenation = new byte[nonceInBytes.Length + createdInBytes.Length + secretInBytes.Length]; 
Array.Copy(nonceInBytes, concatenation, nonceInBytes.Length); 
Array.Copy(createdInBytes, 0, concatenation, nonceInBytes.Length, createdInBytes.Length); 
Array.Copy(secretInBytes, 0, concatenation, (nonceInBytes.Length + createdInBytes.Length), secretInBytes.Length);
return Convert.ToBase64String(SHA1.Create().ComputeHash(concatenation)); 
} 
}

Note the algorithm to create the Password Digest as specified at http://docs.oasis-open.org/wss/v1.1/wss-v1.1-spec-os-UsernameTokenProfile.pdf:

Password_Digest = Base64 ( SHA-1 ( nonce + created + password ) )

Where nonce is short for number used once. It could be a random number and the service use it to prevent replay attacks. Created is the current date and password is the secret word in plain text. Having the password in plain text is the reason why Password Digest is not safe as authentication protocol, it’s definitely a step forward from basic access authentication but it lacks the secure nature of other approaches like Public-key cryptography.

Summarizing, we have created an IEndpointBehavior to attach a IClientMessageInspector instance to the request pipeline. With that IClientMessageInspector we injected custom headers to the request. One of the custom headers was the SecurityHeader which handles the Username Tokens needed to communicate with Password Digest based OASIS Web Service.

Finally, to attach the behavior to the service client we have two options, using xml configuration files (web.config) or directly adding it to the Behaviors property of ServiceEndPoint:

var action = new …;//Create the proxy client 
action.Endpoint.Behaviors.Add(new InspectorBehavior( 
     new ClientInspector(new SecurityHeader(username, password))));