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); } }
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 ) )
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))));
Thanks, this saved my life!!!!
ReplyDeleteThanks, this definitely saved me tons of time...
ReplyDeleteGlad to help
ReplyDeleteThanks so much!
ReplyDeletethanks, worked perfectly!
ReplyDeleteThanks ... WriteHeaders() dont work for me, but the function ComputePasswordDigest() work perfect and solve my problem.
ReplyDeletethanks, worked brilliantly
ReplyDeleteThanks a lot for your post, it has been of great help for me!
ReplyDeleteThanks,
ReplyDeleteIt's only post on internet I found so far which really works. )))
Many thank. This article really saves my time.
ReplyDeleteGreat post. You save a lot of my time.
ReplyDeleteThis is proof of power of internet.
Great post!! It solved my five day torture...
ReplyDeleteI'm speechless... after days of reading all kinds of backward answers on stackoverflow, this finally solves it for me. I can't thank you enough!
ReplyDeleteVery cool, but now I have two Security headers, how can I stop the old one that's useless being put in to the header?
ReplyDeleteI understand the concept about injecting the header, but I don't follow the last two lines: How do I use it?
ReplyDeleteI have
MyWebService mws = new MyWebService();
mws.CallSomething();
Where do I put the
.Endpoint.Behaviors.Add(new InspectorBehavior(new ClientInspector(new SecurityHeader(username, password))));
?
The format used in the security header is "yyyy-MM-ddThh:mm:ss.msZ" and needs to be "yyyy-MM-ddTHH:mm:ss.fffZ" to use 24-hour format (HH) and milliseconds (fff). I had my solution working only during the AM hours because of the bug. ;)
ReplyDeleteOther than that, this was a great help. Thanks!
You can't realize how useful was this post to me
ReplyDeleteThank you so much
Please help me in understanding on how to read the username and password on the service side.
ReplyDeleteWhat needs to be implemented server side(service) to work with this?
ReplyDeleteI implemented the same solution to the username token profile in wcf problem.
ReplyDeleteBut i can't see the values of the username, password and nonce in the messages that i logged.
I suspect that it has somenthing to do about the security properties in the bindings configuration. Can anybody give me a help regarding this? thank you.
I have converted the code over to vb.net and can see the header being added to the endpoint behaviors while debugging but when I send the request and caputure it with fiddler it does not have the header in the soap request. Here is what i'm seeing while debugging but does not seem to attach to the soap request.
ReplyDelete{
username
password
lYBX5PxR6KudJqOAP08tI1lZ7b58MSZ+MSzFQPxT9ELYwS7RSAnMWmV9WXTWPEkgGtj9UT3oIDl966eui+kBzA==
2013-06-18T04:17:38.1738Z
}
Looks like the actual soap header didn't publish right to this post sorry.
Delete"Attaching OASIS Username Tokens headers in WCF Requests"
ReplyDelete+
"WCF Extensibility – Behavior configuration extensions" *
=
Save MY life :D
* http://blogs.msdn.com/b/carlosfigueira/archive/2011/06/28/wcf-extensibility-behavior-configuration-extensions.aspx
Thanks a lot!!!
ReplyDeleteThanks man!!! Really was killing myself about this
ReplyDeleteAlmost 4 years in and this still helped me. Thank you very much
ReplyDeleteThis is a great help, but I'm still trying to work out how to use the ClientInspector by configuring the web.config. If I add a third party's service as a Service Reference, I can access the EndpointBehaviors and add my custom header via code per your example. If I add their service as a Web Reference, there is no exposed EndpointBehaviors property. I've found a few examples indicating I can use web.config to set this up on my client, but I just don't see how it works. In my use case, it looks like I'm closer to my goal with the Web Service setup. Has anyone else applied that successfully with this approach?
ReplyDeleteThis has absolutely saved my day!!! Well done!!!
ReplyDeletethis line
ReplyDeletestring created = DateTime.Now.ToString("yyyy-MM-ddThh:mm:ss.msZ");
needs changing to
string created = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.msZ");
to accomodate for 24 hour time
Spot on, I agree!
DeleteThank you for sharing this knowledge, the only I found after 2 days searching!
ReplyDeleteWow, eres un groso.... de verdad que solucion!!
ReplyDeletealmost 10 years later and you're still saving lives with this post. thanks dog!
ReplyDeleteGod bless you.
ReplyDelete