Dapeng Li's Blog

Saturday, June 27, 2009

 

Add custom security header in WCF


In a recent project I'm working on, we're supposed to call an external web service with request’s header like this:

<soapenv:Header>
<wsse:Security xmlns:wsse="http://schemas.xmlsoap.org/ws/2003/06/secext" soapenv:mustUnderstand="1">
<wsse:UsernameToken>
<wsse:Username>****Username Here****</wsse:Username>
<wsse:Password>****Password Here****</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
As regularly, in Visual Studio 2008 I added a service reference to the web service, then write code to call it using the auto-generated proxy:
Proxy client = new Proxy();
client.DoSomething();
But I got this error message from the service:
System.ServiceModel.FaultException : WSEC5075E: No security token found which satisfies any one of AuthMethods.

So I tried to add name and password to the proxy client before making the call:
Proxy client = new Proxy();
client.ClientCredentials.UserName.UserName = “Username”;
client.ClientCredentials.UserName.Password = “Password”;
client.DoSomething();
Same error as above. After some googling, I think updating the binding config might do the trick.
Updated from:
<system.serviceModel>
<bindings>
<binding name="serviceBinding" ....>
<security mode="Transport">
<transport clientCredentialType="None" proxyCredentialType="None" realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</bindings>
</system.serviceModel>
to:
<system.serviceModel>
<bindings>
<binding name="serviceBinding" ....>
<security mode="TransportWithMessageCredential">
<transport clientCredentialType="None" proxyCredentialType="None" realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</bindings>
</system.serviceModel>
OK, Something different returned from the service:
System.ServiceModel.Security.MessageSecurityException : An unsecured or incorrectly secured fault was received from the other party. See the inner FaultException for the fault code and detail.
----> System.ServiceModel.FaultException : com.ibm.wsspi.wssecurity.SoapSecurityException: The Application Server expected a Security header with the http://schemas.xmlsoap.org/ws/2003/06/secext or http://schemas.xmlsoap.org/ws/2002/07/secext or http://schemas.xmlsoap.org/ws/2002/04/secext namespace, but it was not found.

Seems to be the header is not what the service was expecting, but how can I set the header like that? Google lead me to this post, the author seems have the similar problem, I followed his instruction and added the following:
BindingElementCollection elements = client.Endpoint.Binding.CreateBindingElements();
elements.Find<SecurityBindingElement>().IncludeTimestamp = false;
client.Endpoint.Binding = new CustomBinding(elements);
But my error still the same. I used Charles to see the raw request and response:
Request header:
<s:Header>
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<o:UsernameToken u:Id="uuid-4c248cea-1a3d-4a27-99f0-29ee659ed975-1">
<o:Username>
abc
</o:Username>
<o:Password o:Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">
123
</o:Password>
</o:UsernameToken>
</o:Security>
</s:Header>
The user name and password are in the request, seems only the namespace is not matching the service’s expectation. To verify that, I updated the namespace of security header in the above request in Charles and submitted again, yes! The service is responding correctly. I had no experience in WSE, but guess is that our service is sticking to an earlier version of WS-Security, and rejecting requests with namespace defined in newer versions after that. Maybe that’s why the code above works in the post author’s situation but not mine, maybe.

Found the problem, but how can I update the namespace in the header? Through configuration? Programming?

As one answer in this post said: “Do not define security at all in your client and add the whole security header inside a message inspector.”, this guy seems had given this suggestions in multiple posts and I hadn’t found anything more helpful than that, so I might give a try to write a WCF message inspector.

I found what I want here.
Here’s the code:
        /// <summary>
/// Custom Endpoint Behavior
/// </summary>
public class AddWseSecurityHeaderEndpointBehavior : IEndpointBehavior
{
private readonly string userName;
private readonly string password;

public AddWseSecurityHeaderEndpointBehavior(string userName, string password)
{
this.userName = userName;
this.password = password;
}

#region IEndpointBehavior Members

public void AddBindingParameters(ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{ }

public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
AddWseSecurityHeaderMessageInspector inspector =
new AddWseSecurityHeaderMessageInspector(userName, password);

clientRuntime.MessageInspectors.Add(inspector);
}

public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher)
{ }

public void Validate(ServiceEndpoint endpoint)
{ }

#endregion
}

/// <summary>
/// Custom Message Inspector to add WSE security header to the request before sending
/// </summary>
internal class AddWseSecurityHeaderMessageInspector : IClientMessageInspector, IDispatchMessageInspector
{
private readonly string userName;
private readonly string password;

public AddWseSecurityHeaderMessageInspector(string userName, string password)
{
this.userName = userName;
this.password = password;
}

#region IClientMessageInspector Members

public void AfterReceiveReply(ref Message reply, object correlationState)
{ }

public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
CustomWseSecurityHeader securityHeader =
new CustomWseSecurityHeader(userName, password);

request.Headers.Add(securityHeader);
return request;
}

#endregion

#region IDispatchMessageInspector Members

public object AfterReceiveRequest(ref Message request, IClientChannel channel,
InstanceContext instanceContext)
{
return null;
}

public void BeforeSendReply(ref Message reply, object correlationState)
{ }

#endregion
}

/// <summary>
/// Custom WSE security header
/// </summary>
internal class CustomWseSecurityHeader : MessageHeader
{
private readonly string userName;
private readonly string password;

public CustomWseSecurityHeader(string userName, string password)
{
this.userName = userName;
this.password = password;
}

protected override void OnWriteHeaderContents(XmlDictionaryWriter writer,
MessageVersion messageVersion)
{
writer.WriteStartElement("UsernameToken", Namespace);
writer.WriteElementString("Username", Namespace, userName);
writer.WriteElementString("Password", Namespace, password);
writer.WriteEndElement();
}

public override string Name
{
get { return "Security"; }
}

public override string Namespace
{
get { return "http://schemas.xmlsoap.org/ws/2003/06/secext"; }
}

public override bool MustUnderstand
{
get { return true; }
}
}
Before making calls with the proxy, add our custom endpoint behavior, we don’t need to set the username and password (we’ve done that in the custom header):
Proxy client = new Proxy();

AddWseSecurityHeaderEndpointBehavior customEndpointBehavior =
new AddWseSecurityHeaderEndpointBehavior(userName, password);

client.Endpoint.Behaviors.Add(customEndpointBehavior);

client.DoSomething();
Also as we’re setting username and password in our custom header, we need to change the mode attribute of security element (under binding) to Transport.

Note: Once you've figured out what's the problem with the request message format, you might want to close Charles. Or you will see certificate errors because Charles will act as the middle man between your proxy and the service, the certificate you'll see from the proxy's perspective is issue by Charles, which is not a trusted root CA in your computer.
Firstly, I got confused when I access the web service address from Firefox directly it's displaying, while in IE 8 it says the "The security certificate presented by this website was not issued by a trusted certificate authority.", I can't see the certificate in IE. Then I open that address in Safari, similar prompt, but this time I got a chance to see the service certificate, and see the issuer is Charles.
Comments:
Thank you for this article.
 
hi,
what is the final request xml? Could you please share it.
Regards...
 
Hello admineer,

Unfortunately I don't have access to the platform now, good luck with finding your solution!
 
Post a Comment

Subscribe to Post Comments [Atom]





<< Home

Archives

October 2004   June 2009   January 2010   December 2011  

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]