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))));

34 comments:

  1. Thanks, this saved my life!!!!

    ReplyDelete
  2. Thanks, this definitely saved me tons of time...

    ReplyDelete
  3. Thanks ... WriteHeaders() dont work for me, but the function ComputePasswordDigest() work perfect and solve my problem.

    ReplyDelete
  4. thanks, worked brilliantly

    ReplyDelete
  5. Thanks a lot for your post, it has been of great help for me!

    ReplyDelete
  6. Thanks,
    It's only post on internet I found so far which really works. )))

    ReplyDelete
  7. Many thank. This article really saves my time.

    ReplyDelete
  8. Great post. You save a lot of my time.

    This is proof of power of internet.

    ReplyDelete
  9. Great post!! It solved my five day torture...

    ReplyDelete
  10. I'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!

    ReplyDelete
  11. Very cool, but now I have two Security headers, how can I stop the old one that's useless being put in to the header?

    ReplyDelete
  12. I understand the concept about injecting the header, but I don't follow the last two lines: How do I use it?
    I have
    MyWebService mws = new MyWebService();
    mws.CallSomething();

    Where do I put the
    .Endpoint.Behaviors.Add(new InspectorBehavior(new ClientInspector(new SecurityHeader(username, password))));
    ?

    ReplyDelete
  13. 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. ;)

    Other than that, this was a great help. Thanks!

    ReplyDelete
  14. You can't realize how useful was this post to me
    Thank you so much

    ReplyDelete
  15. Please help me in understanding on how to read the username and password on the service side.

    ReplyDelete
  16. What needs to be implemented server side(service) to work with this?

    ReplyDelete
  17. I implemented the same solution to the username token profile in wcf problem.
    But 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.

    ReplyDelete
  18. 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.

    {

    username
    password
    lYBX5PxR6KudJqOAP08tI1lZ7b58MSZ+MSzFQPxT9ELYwS7RSAnMWmV9WXTWPEkgGtj9UT3oIDl966eui+kBzA==
    2013-06-18T04:17:38.1738Z

    }

    ReplyDelete
    Replies
    1. Looks like the actual soap header didn't publish right to this post sorry.

      Delete
  19. "Attaching OASIS Username Tokens headers in WCF Requests"
    +
    "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

    ReplyDelete
  20. Thanks a lot!!!

    ReplyDelete
  21. Thanks man!!! Really was killing myself about this

    ReplyDelete
  22. Almost 4 years in and this still helped me. Thank you very much

    ReplyDelete
  23. This 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?

    ReplyDelete
  24. This has absolutely saved my day!!! Well done!!!

    ReplyDelete
  25. this line

    string 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

    ReplyDelete
  26. Thank you for sharing this knowledge, the only I found after 2 days searching!

    ReplyDelete
  27. Wow, eres un groso.... de verdad que solucion!!

    ReplyDelete
  28. almost 10 years later and you're still saving lives with this post. thanks dog!

    ReplyDelete