WCF: Adding Nonce to UsernameToken - c#

I'm trying to connect to a web service, written in Java, but there's something I can't figure out.
Using WCF and a customBinding, almost everything seems to be fine, except one part of the SOAP message, as it's missing the Nonce and Created part nodes.
Obviously I'm missing something, so if you could point me into the right direction, it'd be much appreciated.
Here's the custom binding:
<binding name="CustomHTTPBinding">
<security includeTimestamp="false" authenticationMode="UserNameOverTransport" defaultAlgorithmSuite="Basic256" requireDerivedKeys="True"
messageSecurityVersion="WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10">
</security>
<textMessageEncoding maxReadPoolSize="211" maxWritePoolSize="2132" messageVersion="Soap11"
writeEncoding="utf-8"/>
<httpsTransport />
</binding>
And here's the relevant part of the message:
<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-c306efd1-e84c-410e-a2ad-1046b368582e-1">
<o:Username>
<!-- Removed-->
</o:Username>
<o:Password>
<!-- Removed-->
</o:Password>
</o:UsernameToken>
</o:Security>
And this's how it should look:
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soapenv:mustUnderstand="1">
<wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="UsernameToken-25763165">
<wsse:Username>..</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">..</wsse:Password>
<wsse:Nonce>6ApOnLn5Aq9KSH46pzzcZA==</wsse:Nonce>
<wsu:Created>2009-05-13T18:59:23.309Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
So the question is: How could I introduce the Nonce and Created elements inside the security part?

To create the nonce, I had to change a few things
First, added a custom binding in my config
<system.serviceModel>
<bindings>
<customBinding>
<binding name="myCustomBindingConfig">
<security includeTimestamp="false"
authenticationMode="UserNameOverTransport"
defaultAlgorithmSuite="Basic256"
requireDerivedKeys="true"
messageSecurityVersion="WSSecurity10WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10">
</security>
<textMessageEncoding messageVersion="Soap11"></textMessageEncoding>
<httpsTransport maxReceivedMessageSize="2000000000" />
</binding>
</customBinding>
</bindings>
</system.serviceModel>
<client>
<endpoint address="https://..." [other tags]
binding="customBinding" bindingConfiguration="OrangeLeapCustomBindingConfig"/>
</client>
Then, take this code found here: http://social.msdn.microsoft.com/Forums/en-US/wcf/thread/4df3354f-0627-42d9-b5fb-6e880b60f8ee
and modify it to create the nonce (just a random hash, base-64 encoded)
protected override void WriteTokenCore(System.Xml.XmlWriter writer, System.IdentityModel.Tokens.SecurityToken token)
{
Random r = new Random();
string tokennamespace = "o";
DateTime created = DateTime.Now;
string createdStr = created.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
string nonce = Convert.ToBase64String(Encoding.ASCII.GetBytes(SHA1Encrypt(created + r.Next().ToString())));
System.IdentityModel.Tokens.UserNameSecurityToken unToken = (System.IdentityModel.Tokens.UserNameSecurityToken)token;
writer.WriteRaw(String.Format(
"<{0}:UsernameToken u:Id=\"" + token.Id + "\" xmlns:u=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" +
"<{0}:Username>" + unToken.UserName + "</{0}:Username>" +
"<{0}:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText\">" +
unToken.Password + "</{0}:Password>" +
"<{0}:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">" +
nonce + "</{0}:Nonce>" +
"<u:Created>" + createdStr + "</u:Created></{0}:UsernameToken>", tokennamespace));
}
protected String ByteArrayToString(byte[] inputArray)
{
StringBuilder output = new StringBuilder("");
for (int i = 0; i < inputArray.Length; i++)
{
output.Append(inputArray[i].ToString("X2"));
}
return output.ToString();
}
protected String SHA1Encrypt(String phrase)
{
UTF8Encoding encoder = new UTF8Encoding();
SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
byte[] hashedDataBytes = sha1Hasher.ComputeHash(encoder.GetBytes(phrase));
return ByteArrayToString(hashedDataBytes);
}

I had the same problem. Instead of the custom token serializer I used a MessageInspector to add the correct UsernameToken in the BeforeSendRequest method. I then used a custom behavior to apply the fix.
The entire process is documented (with a demo project) in my blog post Supporting the WS-I Basic Profile Password Digest in a WCF client proxy. Alternatively, you can just read the PDF.
If you want to follow my progress through to the solution, you'll find it on StackOverflow titled, "Error in WCF client consuming Axis 2 web service with WS-Security UsernameToken PasswordDigest authentication scheme":

It's worth pointing out that Rick Strahl made a blog post (which he references this question) where he explains it all quite clearly and offers solutions for both just Password and also PasswordDigest.
I post this because I found this article originally, couldn't really follow it, and found Rick's post much later. This might save some people some time.
WCF WSSecurity and WSE Nonce Authentication

I also had to put a UserNameHeader segment in the SOAP message header:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:bar:services" xmlns:efm="urn:bar:services">
<soapenv:Header>
<efm:UserNameHeader>
<UserName>foouser</UserName>
<Password>foopass</Password>
</efm:UserNameHeader>
</soapenv:Header>
<soapenv:Body>
<urn:GetUserList/>
</soapenv:Body>
</soapenv:Envelope>
This was accomplished with a custom message header:
public class UserNamePasswordHeader : MessageHeader
{
private readonly string _serviceUserEmail;
private readonly string _serviceUserPassword;
public UserNamePasswordHeader(string serviceUserEmail, string serviceUserPassword)
{
this._serviceUserEmail = serviceUserEmail;
this._serviceUserPassword = serviceUserPassword;
}
public override string Name
{
get { return "UserNameHeader"; }
}
public override string Namespace
{
get { return "urn:bar:services"; }
}
protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteElementString("UserName", _serviceUserEmail);
writer.WriteElementString("Password", _serviceUserPassword);
}
}
Other tags, such as Nonce and Created, could easily be added.
The class is used as follows:
var service = new BarServiceClient();
service.ClientCredentials.ClientCertificate.Certificate = MessageSigningCertificate;
using (new OperationContextScope(service.InnerChannel))
{
OperationContext.Current.OutgoingMessageHeaders.Add(
new UserNamePasswordHeader(serviceUserEmail, serviceUserPassword));
try
{
var response = service.GetUserList();
return response;
}
finally
{
service.Close();
}
}
Note: MessageSigningCertificate is an X.509 certificate, I read it from a file:
private static X509Certificate2 LoadCertificateFromFile(string pfxFilePath, string privateKeyPassword)
{
// Load the certificate from a file, specifying the password
var certificate = new X509Certificate2(pfxFilePath, privateKeyPassword);
return certificate;
}

Related

WCF Service SSRS works via URL on my browser but not on WCF Client?

On my client with NTLM authentication with username and password I can login on browser and but reports via WCF SSRS consuming don't work.
Please see the code attached.
Generate Report Function
public byte[] GenerateReport(string reportName, ReportingService.ExportFormat format,IList<MyApplicationService.ParameterValue> parameters, out string mimeType)
{
byte[] output;
string extension, encoding;
const string reportDirectory = "/Reports/";
MyApplicationService.Warning[] warnings;
string[] streamIds;
ReprotServ.ReportExporter.Export("basicHttpEndpoint", new NetworkCredential("Username", "Password"),
reportDirectory + reportName, parameters.ToArray(), format, out output, out extension, out mimeType, out encoding, out warnings, out streamIds);
return output;
}
public static void Export(
string wcfEndpointConfigName, System.Net.NetworkCredential clientCredentials, string report, ParameterValue[] parameters,
ExportFormat format, out byte[] output, out string extension, out string mimeType, out string encoding, out Warning[] warnings, out string[] streamIds)
{
var webServiceProxy = new MyApplicationService.ReportExecutionServiceSoapClient(wcfEndpointConfigName);
try
{
webServiceProxy.ClientCredentials.Windows.AllowedImpersonationLevel =
System.Security.Principal.TokenImpersonationLevel.Impersonation;
webServiceProxy.ClientCredentials.Windows.ClientCredential = clientCredentials;
// Init Report to execute
ServerInfoHeader serverInfoHeader;
ExecutionInfo executionInfo;
ExecutionHeader executionHeader = webServiceProxy.LoadReport(null, report, null, out serverInfoHeader, out executionInfo);
// Attach Report Parameters
webServiceProxy.SetExecutionParameters(executionHeader, null, parameters, null, out executionInfo);
// Render
serverInfoHeader =
webServiceProxy.Render(executionHeader, null, GetExportFormatString(format), null,
out output, out extension, out mimeType, out encoding, out warnings,
out streamIds);
webServiceProxy.Close();
}
catch (CommunicationException e)
{
webServiceProxy.Abort();
throw new CommunicationException(e.Message);
}
finally
{
webServiceProxy.Close();
}
Web Config File
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="ReportExecutionServiceSoap" receiveTimeout="10:00:00" sendTimeout="10:00:00" allowCookies="true" maxReceivedMessageSize="5242880">
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Ntlm" proxyCredentialType="None" realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://xyz:80/ReportServer/ReportExecution2005.asmx"
binding="basicHttpBinding" bindingConfiguration="ReportExecutionServiceSoap"
contract="MyApplicationService.ReportExecutionServiceSoap" name="basicHttpEndpoint" />
</client>
</system.serviceModel>
For More clarification here is the picture attached with error message.
Things which I have done.
Stopped using using keyword.. // Nothing worked .
Service is working on my browser so how can it be faulted.
Tried .Net Tracing didn't get any help since the error message remain the same.
Tried using WCFTestClient.exe also data is there but via client writing code it gives me error.
State is sometimes Opened, Mostly faulted but endpoint written via quick watch shows its null as shown in picture below. But I get something in URL address.
Can some one guide me. I been stuck here for weeks.

How to Add OrganizationToken to WCF Endpoint in code

I am trying to connect to a SOAP API of a vendor and I have a sample project they provided which works. I am trying to figure out how to replace their endpoint which is in the app.config with one created in code so I can load endpoint URL, username, and password using DI.
Here is the relevant XML from the app.config:
<client>
<endpoint address="https://strongmail.com/sm/services/mailing/v2" binding="basicHttpBinding" bindingConfiguration="MailingServiceServiceSoapBinding" contract="MailingService" name="MailingServicePort">
<headers>
<SOAP-ENV:Header xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsse:UsernameToken wsu:Id="UsernameToken">
<wsse:Username>Username</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">Password</wsse:Password>
</wsse:UsernameToken>
<OrganizationToken xmlns="http://www.strongmail.com/services/v2/schema">
<organizationName>admin</organizationName>
<subOrganizationId>
<id>1</id>
</subOrganizationId>
</OrganizationToken>
</wsse:Security>
</SOAP-ENV:Header>
</headers>
</endpoint>
</client>
This is the code I am trying to use to instantiate this.
public class SelligentOrganizationToken
{
public string organizationName { get; set; }
public SelligentOrganization subOrganizationId { get; set; }
}
public class SelligentOrganization
{
public string id { get; set; }
}
private MailingService CreateMailingService(string mailingServiceUrl, string userName, string password)
{
var securityElement = SecurityBindingElement.CreateUserNameOverTransportBindingElement();
securityElement.IncludeTimestamp = false;
var encodingElement = new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8);
var transportElement = new HttpsTransportBindingElement();
var customBinding = new CustomBinding(securityElement, encodingElement, transportElement);
var remoteAddress = new EndpointAddress(mailingServiceUrl);
var mailingService = new MailingServiceClient(customBinding, remoteAddress);
mailingService.ClientCredentials.UserName.UserName = userName;
mailingService.ClientCredentials.UserName.Password = password;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11;
var selligentOrganizationToken = new SelligentOrganizationToken()
{
organizationName = "admin",
subOrganizationId = new SelligentOrganization()
{
id = "1"
}
};
//Serialize object to xml
XmlObjectSerializer xmlObjectSerializer = new DataContractSerializer(typeof(SelligentOrganizationToken), "OrganizationToken", "http://www.strongmail.com/services/v2/schema");
var eab = new EndpointAddressBuilder(mailingService.Endpoint.Address);
eab.Headers.Add(AddressHeader.CreateAddressHeader("OrganizationToken", "http://www.strongmail.com/services/v2/schema", selligentOrganizationToken, xmlObjectSerializer));
mailingService.Endpoint.Address = eab.ToEndpointAddress();
return mailingService;
}
When I try to run this I am getting:
System.ServiceModel.FaultException: 'A security error was encountered when verifying the message'
Which, incidentally, is what I get when running the example code if I remove the OrganizationToken node.
Any help on this will be greatly appreciated, thank you in advance!
I see you app.config has configured headers node and it seems you are not using the endpointconfig. Instead, you add headers in your c# code.
How about directly use the endpoint configuration in your app.config?
var mailingService = new MailingServiceClient("MailingServicePort")
If you want to add addressHeader, you could also try to use OutgoingMessageHeaders or HttpRequestMessageProperty.
https://code.msdn.microsoft.com/windowsapps/How-to-add-custom-5cbbf066
Please ensure where you want to add the header, OutgoingMessageHeaders is used to add a soap header and HttpRequestMessageProperty is used to add a header in request header.

C# ServiceReference - The response message does not match the content type of the binding

I've read over 100 articles and answers I got nothing, so hopefully you can help (sorry if I cannot find proper one, even if it exists).
I got a class library (.NET Framework) with 4 Service References to SOAP endpoints. In case it matters - the endpoint is a black box, I just got wsdl and I need to connect there.
The library is loaded by the .exe that I cannot change, so even when I tried to use app.config, I cannot (problem is rather known), because it should be added to .exe config, so I use those references but I create a binding by myself and use ChannelFactory as a proxy to call the web method. I tried those 3 types - BasicHttpBinding, BasicHttpsBinding and BasicHttpContextBinding. All binding properties are the same as in app.config generated by the reference.
After calling a method from an endpoint I got a message, that is very popular on SO:
The content type text/html; charset=utf-8 of the response message does not match the content type of the binding (text/xml; charset=utf-8). If using a custom encoder, be sure that the IsContentTypeSupported method is implemented properly
But answers for this problem aren't helpful for me as the reason why I'm getting this error, is because of the request that instead of SOAP is a following HTML:
<head>
<script>
var title = "<title>" + location.host + " - webMethods Integration Server" + "</title>";
document.write(title);
</script>
<LINK REL="SHORTCUT ICON" HREF="/favicon.ico">
</head>
<frameset rows="100%,*" border=0><frame src="/WmRoot/index.dsp"></frameset>
<noframes>
<body>
<h4>
Your browser does not support frames. Support for frames is required to use the webMethods Integration Server.
</h4>
</blockquote>
</body>
</noframes>
I tried also creating SOAP envelope by myself and connect to the server directly with no service reference, but I got the same message.
Nevertheless, if I try to connect from SaopUI I get proper response.
What am I doing wrong?
#EDIT : adding code as #Tim asked
My code - 2 ways, both responding the same...
Way with no ChannelFactory
I call a method from the class of the type of the service from reference (I merged 2 methods, because originally in my code it is divided between creating a client and then opening and calling the method - this is in another part of application, but it works similary to this one):
public virtual FindCustResponse FindCust (FindCustRequest customer)
{
FindCustResponse result = null;
Services.FindCustomer.findCustomer_WSD_PortTypeClient client = null;
client = new Services.FindCustomer.findCustomer_WSD_PortTypeClient(FindCustomerHTTPsBinding(), GetServiceEndpoint(_serviceConfigProvider.
RemoteAddress));
if (client != null)
{
try
{
ClientCredentials loginCredentials = new ClientCredentials();
loginCredentials.UserName.UserName = _serviceConfigProvider.User;
loginCredentials.UserName.Password = _serviceConfigProvider.Password;
var defaultCredentials = client.Endpoint.Behaviors.Find<ClientCredentials>();
client.Endpoint.Behaviors.Remove(defaultCredentials);
client.Endpoint.Behaviors.Add(loginCredentials);
client.Open();
result = client.findCustomer(customer);
}
catch (Exception ex)
{
//Logging
}
finally
{
if (client.State == CommunicationState.Opened || client.State == CommunicationState.Opening)
{
client.Close();
}
}
}
return client;
}
Another way - with ChannelFacotry
public virtual Services.FindCustomer.ImyService_WSD_PortType FindCustClient()
{
var client = new ChannelFactory<Services.FindCustomer.IMyService_WSD_PortType (FindCustomerHTTPsBinding(), GetServiceEndpoint(_serviceConfigProvider.RemoteAddress));
ClientCredentials loginCredentials = new ClientCredentials();
loginCredentials.UserName.UserName = _serviceConfigProvider.User;
loginCredentials.UserName.Password = _serviceConfigProvider.Password;
var defaultCredentials = client.Endpoint.Behaviors.Find<ClientCredentials>();
client.Endpoint.Behaviors.Remove(defaultCredentials);
client.Endpoint.Behaviors.Add(loginCredentials);
var proxy = client.CreateChannel();
return proxy;
}
public FindCustResponse1 FindCust (FindCustRequest1 customer)
{
var customerresults = _service.FindCustClient().findCustomer(customer);
return customerresults;
}
Bindings that I've tried in both cases:
public virtual BasicHttpContextBinding FindCustomerContextBinding()
{
var binding = new BasicHttpContextBinding
{
ContextManagementEnabled = true,
MessageEncoding = WSMessageEncoding.Text,
TextEncoding = System.Text.Encoding.UTF8,
SendTimeout = TimeSpan.FromMinutes(1),
HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
TransferMode = TransferMode.Buffered,
Security = { Mode = BasicHttpSecurityMode.Transport },
Name = ConfigConstants.FindCustomerBinding
};
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
return binding;
}
public virtual System.ServiceModel.BasicHttpsBinding FindCustomerHTTPsBinding()
{
var binding = new BasicHttpsBinding
{
TextEncoding = System.Text.Encoding.UTF8,
SendTimeout = TimeSpan.FromMinutes(1),
HostNameComparisonMode = HostNameComparisonMode.StrongWildcard, // tried both - with and without
TransferMode = TransferMode.Buffered,
Security = { Mode = BasicHttpsSecurityMode.Transport },
Name = ConfigConstants.FindCustomerBinding
};
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
return binding;
}
public virtual BasicHttpBinding FindCustomerBasicBinding()
{
var binding = new BasicHttpBinding
{
TextEncoding = System.Text.Encoding.UTF8,
SendTimeout = TimeSpan.FromMinutes(1),
TransferMode = TransferMode.Buffered,
Security = { Mode = BasicHttpSecurityMode.Transport },
// Tried alsow with: TransportWithMessageCredential, Message = { ClientCredentialType = BasicHttpMessageCredentialType.UserName}
Name = ConfigConstants.FindCustomerBinding,
};
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
return binding;
}
This is how I get the endpoint address:
public virtual EndpointAddress GetServiceEndpoint(string address)
{
if (!Uri.IsWellFormedUriString(address, UriKind.Absolute)) {return null;}
var uri = new Uri(address);
var endpoint = new EndpointAddress(uri.AbsoluteUri);
return endpoint;
}
And my generated app.config (which I don't use as I mentioned) is:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
</configSections>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="SomeBindingname_GET_Binder">
<security mode="Transport"/>
</binding>
<binding name="SomeBindingname_GET_Binder1"/>
<binding name="SomeBindingname_ADD_Binder">
<security mode="Transport"/>
</binding>
<binding name="SomeBindingname_ADD_Binder1"/>
<binding name="SomeBindingname_UPDATE_Binder">
<security mode="Transport"/>
</binding>
<binding name="SomeBindingname_UPDATE_Binder1"/>
<binding name="SomeBindingname_FIND_Binder">
<security mode="Transport"/>
</binding>
<binding name="SomeBindingname_FIND_Binder1"/>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="https://myServiceAddress:11111/ws/veryLongNameGETCustomer" binding="basicHttpBinding" bindingConfiguration="SomeBindingname_GET_Binder" contract="GetCustomer.getCustomer_WSD_PortType" name="getCustomer_WSD_Port"/>
<endpoint address="https://myServiceAddress:11111/ws/veryLongNameAddCustomer" binding="basicHttpBinding" bindingConfiguration="SomeBindingname_ADD_Binder" contract="AddCustomer.addCustomer_WSD_PortType" name="addCustomer_WSD_Port"/>
<endpoint address="https://myServiceAddress:11111/ws/veryLongNameUpdateCustomer" binding="basicHttpBinding" bindingConfiguration="SomeBindingname_UPDATE_Binder" contract="UpdateCustomer.updateCustomer_WSD_PortType" name="updateCreateCustomer_WSD_Port"/>
<endpoint address="https://myServiceAddress:11111/ws/veryLongNameFindCustomer" binding="basicHttpBinding" bindingConfiguration="SomeBindingname_FIND_Binder" contract="FindCustomer.findCustomer_WSD_PortType" name="findCustomer_WSD_Port"/>
</client>
</system.serviceModel>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/></startup></configuration>

(Attempting to) migrate from WSE 3.0 to WCF for client code

I have been all over the net for this. I've just been having a devil of a time doing it, and the vendor whose web service I'm trying to consume refuses to officially support WCF as a method of consumption.
I'm no web services expert, so I'll do my best to document and explain with this initial post, but by all means, request more information if you need it, and hopefully I'll be able to supply whatever is necessary.
The service
At my company, we use a vendor application that exposes a service. The application is written in java, and it looks like the wsdl was created with Apache Axis 1.2.
The code
My legacy code uses WSE 3.0. In particular, it uses the proxy classes that have "WSE" auto-tacked at the end. This allows me to use a much simpler authentication scheme (the only way I could get it to work). I don't need to use certificates. I use a derivative of SecurityPolicyAssertion, and wrap it in a Policy object that gets passed to the SetPolicy method of the client class. Here's all I need to do to create a working instance of the client:
MyWebServiceWse api = new MyWebServiceWse();
api.Url = myUrl;
api.SetPolicy(new Policy(new MyDerivedSecurityAssertion(user, pass)));
My default, out-of-the-box code for WCF (generated with a service reference) does not accept credentials, so I know there's a problem right off the bat. I've read various things online about using different security or binding settings in my app.config, but nothing has ever completely worked. My most common error after copious tinkering is WSDoAllReceiver: Request does not contain required Security header.
Here's the app.config. Perhaps we could start by telling me what ought to change here to facilitate passing the credentials--again, I've seen varying opinions online.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="MySoapBinding" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None" proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://xyz:12345/services/MyService"
binding="basicHttpBinding" bindingConfiguration="MySoapBinding"
contract="MyNS.MyService" name="MyService" />
</client>
</system.serviceModel>
</configuration>
I have changed some of the attributes to obscure the specific service we are using (company policy and all that).
And here is the sample C# code so far (testing in a console app):
MyClient client = new MyClient();
client.listMethod();
UPDATE
Read this SO post: wcf security . . ..
I have updated my app.config accordingly, and am now passing username and pwd in code. I am still receiving the same error:
WSDoAllReceiver: Request does not contain required Security header
20120517 UPDATE
A successful request (from WSE3):
<soap:Header>
<wsa:Action>
</wsa:Action>
<wsa:MessageID>urn:uuid:cb739422-c077-4eec-8cb2-686837b76878</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:To>http://removed-for-security</wsa:To>
<wsse:Security soap:mustUnderstand="1">
<wsu:Timestamp wsu:Id="Timestamp-e13feaf9-33d9-47bf-ab5b-60b4611eb81a">
<wsu:Created>2012-05-17T11:25:41Z</wsu:Created>
<wsu:Expires>2012-05-17T11:30:41Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="SecurityToken-00c26e1a-3b3b-400f-a99a-3aa54cf8c8ff">
<wsse:Username>change-to-protect-the-innocent</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">nice-try</wsse:Password>
<wsse:Nonce>KJMvUuWF2eO2uIJCuxJC4A==</wsse:Nonce>
<wsu:Created>2012-05-17T11:25:41Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
<soap:Body>
<listChannels xmlns="http://removed-for-security">
<rowfrom>0</rowfrom>
<rowto>10</rowto>
</listChannels>
</soap:Body>
</soap:Envelope>
Working on getting the WCF trace--will add shortly.
20120517 UPDATE 2
And here's the envelope from WCF:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none"></Action>
</s:Header>
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<listChannels xmlns="http://removed-for-security">
<rowfrom>1</rowfrom>
<rowto>2147483647</rowto>
</listChannels>
</s:Body>
</s:Envelope>
20120518 UPDATE
I have tried implementing the solution in the post that Mike Miller links to in the comments. Now I receive the following error (no message ends up getting sent because something's barfing on the scheme):
The provided URI scheme 'http' is invalid; expected 'https'.
And in case anyone wants to ask, yes, I need to send over http, and yes, I'm aware that credentials are sent as unencrypted strings :-)
What you need is to send a username token over http transport which is not supported in wcf ootb. in addition your token uses nonce/created which is also not ootb. you have 2 options:
this oss project adds nonce/created to the username token. this oss project adds the ability to send username over http. you would need to combine both projects together.
ws-security is usually considered complex, but you use it in its simplest form (username). the easiest would be to dismiss any wcf security setting all together and create the whole security header by yourself in a message inspector! As you can see most headers are just static xml nodes, and most values are pretty clear (you know the username). the only tricky two are the nonce and the timestamps which you could look how to do in this oss project (one line each). There is a variant of this option which may be easier - use CUB after all and implement a custom encoder which pushes the timestmpa/nonce. I would go for the latter but I'm biased since I developed CUB...
There's also the ws-addressing headers which you can configure on your custom encoding "messageVersion" property. I can't tell the exact value since you omitted the envelope header with the wsa prefix definition.
If you want help privately (since you seem to have security restrictions) by all means send me an email from my blog.
EDIT: I've implemented it for you. follow these steps:
download cub and make yourself familiar with it (not the internals, just how to use it according to the blog post)
add reference to System.Runtime.Serialization.dll to the project ClearUsernameBinding
add a new file to that project: UsernameExEncoder.cs. Paste this content:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Channels;
using System.IO;
using System.Xml;
using System.Security.Cryptography;
namespace Webservices20.BindingExtensions
{
class UsernameExEncoderBindingElement : MessageEncodingBindingElement
{
MessageEncodingBindingElement inner;
public UsernameExEncoderBindingElement(MessageEncodingBindingElement inner)
{
this.inner = inner;
}
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
var res = base.BuildChannelFactory<TChannel>(context);
return res;
}
public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
{
var res = base.CanBuildChannelFactory<TChannel>(context);
return res;
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new UsernameExEncoderFactory(this.inner.CreateMessageEncoderFactory());
}
public override MessageVersion MessageVersion
{
get
{
return this.inner.MessageVersion;
}
set
{
this.inner.MessageVersion = value;
}
}
public override BindingElement Clone()
{
var c = (MessageEncodingBindingElement)this.inner.Clone();
var res = new UsernameExEncoderBindingElement(c);
return res;
}
public override T GetProperty<T>(BindingContext context)
{
var res = this.inner.GetProperty<T>(context);
return res;
}
}
class UsernameExEncoderFactory : MessageEncoderFactory
{
MessageEncoderFactory inner;
public UsernameExEncoderFactory(MessageEncoderFactory inner)
{
this.inner = inner;
}
public override MessageEncoder Encoder
{
get { return new UsernameExEncoder(inner.Encoder); }
}
public override MessageVersion MessageVersion
{
get { return this.inner.MessageVersion; }
}
}
class UsernameExEncoder : MessageEncoder
{
MessageEncoder inner;
public override T GetProperty<T>()
{
return inner.GetProperty<T>();
}
public UsernameExEncoder(MessageEncoder inner)
{
this.inner = inner;
}
public override string ContentType
{
get { return this.inner.ContentType; }
}
public override string MediaType
{
get { return this.inner.MediaType; }
}
public override MessageVersion MessageVersion
{
get { return this.inner.MessageVersion; }
}
public override bool IsContentTypeSupported(string contentType)
{
return this.inner.IsContentTypeSupported(contentType);
}
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
return this.inner.ReadMessage(buffer, bufferManager, contentType);
}
public override Message ReadMessage(System.IO.Stream stream, int maxSizeOfHeaders, string contentType)
{
return this.inner.ReadMessage(stream, maxSizeOfHeaders, contentType);
}
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
//load the message to dom
var mem = new MemoryStream();
var x = XmlWriter.Create(mem);
message.WriteMessage(x);
x.Flush();
mem.Flush();
mem.Position = 0;
XmlDocument doc = new XmlDocument();
doc.Load(mem);
//add the missing elements
var token = doc.SelectSingleNode("//*[local-name(.)='UsernameToken']");
var created = doc.CreateElement("Created", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
var nonce = doc.CreateElement("Nonce", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
token.AppendChild(created);
token.AppendChild(nonce);
//set nonce value
byte[] nonce_bytes = new byte[16];
RandomNumberGenerator rndGenerator = new RNGCryptoServiceProvider();
rndGenerator.GetBytes(nonce_bytes);
nonce.InnerText = Convert.ToBase64String(nonce_bytes);
//set create value
created.InnerText = XmlConvert.ToString(DateTime.Now.ToUniversalTime(), "yyyy-MM-ddTHH:mm:ssZ");
//create a new message
var r = XmlReader.Create(new StringReader(doc.OuterXml));
var newMsg = Message.CreateMessage(message.Version, message.Headers.Action, r);
return this.inner.WriteMessage(newMsg, maxMessageSize, bufferManager, messageOffset);
}
public override void WriteMessage(Message message, System.IO.Stream stream)
{
this.inner.WriteMessage(message, stream);
}
}
}
In the file ClearUsernameBinding.cs replace this:
res.Add(new TextMessageEncodingBindingElement() { MessageVersion = this.messageVersion});
with this:
var textEncoder = new TextMessageEncodingBindingElement() { MessageVersion = this.messageVersion };
res.Add(new UsernameExEncoderBindingElement(textEncoder));
In the project TestClient in app.config there is a messageVersion property on the binding element. You have not published the root of your envelope so I cannot know for sure, but probably you need to set it to Soap11WSAddressingAugust2004 or Soap11WSAddressing10 (or one of these with Soap12 instead).
Good luck!

How to sign an Amazon web service request in .NET with SOAP and without WSE

The Amazon Product Advertising API (formerly Amazon Associates Web Service or Amazon AWS) has implemented a new rule which is by August 15th 2009 all web service requests to them must be signed. They have provided sample code on their site showing how to do this in C# using both REST and SOAP. The implementation that I’m using is SOAP. You can find the sample code here, I’m not including it because there is a fair amount.
The problem I’m having is their sample code uses WSE 3 and our current code doesn’t use WSE. Does anyone know how to implement this update with just using the auto generated code from the WSDL? I’d like to not have to switch over to the WSE 3 stuff right now if I don’t have to since this update is more of a quick patch to hold us over until we can fully implement this in the current dev version (August 3rd they’re starting to drop 1 in 5 requests, in the live environment, if they aren’t signed which is bad news for our application).
Here’s a snippet of the main portion that does the actual signing of the SOAP request.
class ClientOutputFilter : SoapFilter
{
// to store the AWS Access Key ID and corresponding Secret Key.
String akid;
String secret;
// Constructor
public ClientOutputFilter(String awsAccessKeyId, String awsSecretKey)
{
this.akid = awsAccessKeyId;
this.secret = awsSecretKey;
}
// Here's the core logic:
// 1. Concatenate operation name and timestamp to get StringToSign.
// 2. Compute HMAC on StringToSign with Secret Key to get Signature.
// 3. Add AWSAccessKeyId, Timestamp and Signature elements to the header.
public override SoapFilterResult ProcessMessage(SoapEnvelope envelope)
{
var body = envelope.Body;
var firstNode = body.ChildNodes.Item(0);
String operation = firstNode.Name;
DateTime currentTime = DateTime.UtcNow;
String timestamp = currentTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
String toSign = operation + timestamp;
byte[] toSignBytes = Encoding.UTF8.GetBytes(toSign);
byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
HMAC signer = new HMACSHA256(secretBytes); // important! has to be HMAC-SHA-256, SHA-1 will not work.
byte[] sigBytes = signer.ComputeHash(toSignBytes);
String signature = Convert.ToBase64String(sigBytes); // important! has to be Base64 encoded
var header = envelope.Header;
XmlDocument doc = header.OwnerDocument;
// create the elements - Namespace and Prefix are critical!
XmlElement akidElement = doc.CreateElement(
AmazonHmacAssertion.AWS_PFX,
"AWSAccessKeyId",
AmazonHmacAssertion.AWS_NS);
akidElement.AppendChild(doc.CreateTextNode(akid));
XmlElement tsElement = doc.CreateElement(
AmazonHmacAssertion.AWS_PFX,
"Timestamp",
AmazonHmacAssertion.AWS_NS);
tsElement.AppendChild(doc.CreateTextNode(timestamp));
XmlElement sigElement = doc.CreateElement(
AmazonHmacAssertion.AWS_PFX,
"Signature",
AmazonHmacAssertion.AWS_NS);
sigElement.AppendChild(doc.CreateTextNode(signature));
header.AppendChild(akidElement);
header.AppendChild(tsElement);
header.AppendChild(sigElement);
// we're done
return SoapFilterResult.Continue;
}
}
And that gets called like this when making the actual web service call
// create an instance of the serivce
var api = new AWSECommerceService();
// apply the security policy, which will add the require security elements to the
// outgoing SOAP header
var amazonHmacAssertion = new AmazonHmacAssertion(MY_AWS_ID, MY_AWS_SECRET);
api.SetPolicy(amazonHmacAssertion.Policy());
I ended up updating the code to use WCF since that's what it is in the current dev version I've been working on. Then I used some code that was posted on the Amazon forums, but made it a little easier to use.
UPDATE: new easier to use code that lets you still use the config settings for everything
In the previous code I posted, and what I've seen elsewhere, when the service object is created one of the constructor overrides is used to tell it to use HTTPS, give it the HTTPS url and to manually attach the message inspector that will do the signing. The downfall to not using the default constructor is you lose the ability to configure the service via the config file.
I've since redone this code so you can continue to use the default, parameterless, constructor and configure the service via the config file. The benifit of this is you don't have to recompile your code to use this, or make changes once deployed such as to maxStringContentLength (which is what caused this change to take place as well as discover the downfalls to doing it all in code). I also updated the signing part a bit so that way you can tell it what hashing algorithm to use as well as the regex for extracting the Action.
These two changes are because not all web services from Amazon use the same hashing algorithm and the Action might need to be extracted differently. This means you can reuse the same code for each service type just by changing what’s in the config file.
public class SigningExtension : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(SigningBehavior); }
}
[ConfigurationProperty("actionPattern", IsRequired = true)]
public string ActionPattern
{
get { return this["actionPattern"] as string; }
set { this["actionPattern"] = value; }
}
[ConfigurationProperty("algorithm", IsRequired = true)]
public string Algorithm
{
get { return this["algorithm"] as string; }
set { this["algorithm"] = value; }
}
[ConfigurationProperty("algorithmKey", IsRequired = true)]
public string AlgorithmKey
{
get { return this["algorithmKey"] as string; }
set { this["algorithmKey"] = value; }
}
protected override object CreateBehavior()
{
var hmac = HMAC.Create(Algorithm);
if (hmac == null)
{
throw new ArgumentException(string.Format("Algorithm of type ({0}) is not supported.", Algorithm));
}
if (string.IsNullOrEmpty(AlgorithmKey))
{
throw new ArgumentException("AlgorithmKey cannot be null or empty.");
}
hmac.Key = Encoding.UTF8.GetBytes(AlgorithmKey);
return new SigningBehavior(hmac, ActionPattern);
}
}
public class SigningBehavior : IEndpointBehavior
{
private HMAC algorithm;
private string actionPattern;
public SigningBehavior(HMAC algorithm, string actionPattern)
{
this.algorithm = algorithm;
this.actionPattern = actionPattern;
}
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)
{
clientRuntime.MessageInspectors.Add(new SigningMessageInspector(algorithm, actionPattern));
}
}
public class SigningMessageInspector : IClientMessageInspector
{
private readonly HMAC Signer;
private readonly Regex ActionRegex;
public SigningMessageInspector(HMAC algorithm, string actionPattern)
{
Signer = algorithm;
ActionRegex = new Regex(actionPattern);
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
var operation = GetOperation(request.Headers.Action);
var timeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
var toSignBytes = Encoding.UTF8.GetBytes(operation + timeStamp);
var sigBytes = Signer.ComputeHash(toSignBytes);
var signature = Convert.ToBase64String(sigBytes);
request.Headers.Add(MessageHeader.CreateHeader("AWSAccessKeyId", Helpers.NameSpace, Helpers.AWSAccessKeyId));
request.Headers.Add(MessageHeader.CreateHeader("Timestamp", Helpers.NameSpace, timeStamp));
request.Headers.Add(MessageHeader.CreateHeader("Signature", Helpers.NameSpace, signature));
return null;
}
private string GetOperation(string request)
{
var match = ActionRegex.Match(request);
var val = match.Groups["action"];
return val.Value;
}
}
To use this you don't need to make any changes to your existing code, you can even put the signing code in a whole other assembly if need be. You just need to set up the config section as so (note: the version number is important, without it matching the code will not load or run)
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="signer" type="WebServices.Amazon.SigningExtension, AmazonExtensions, Version=1.3.11.7, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<behaviors>
<endpointBehaviors>
<behavior name="AWSECommerceBehaviors">
<signer algorithm="HMACSHA256" algorithmKey="..." actionPattern="\w:\/\/.+/(?<action>.+)" />
</behavior>
</endpointBehaviors>
</behaviors>
<bindings>
<basicHttpBinding>
<binding name="AWSECommerceServiceBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true" maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536">
<readerQuotas maxDepth="32" maxStringContentLength="16384" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="Transport">
<transport clientCredentialType="None" proxyCredentialType="None" realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="https://ecs.amazonaws.com/onca/soap?Service=AWSECommerceService" behaviorConfiguration="AWSECommerceBehaviors" binding="basicHttpBinding" bindingConfiguration="AWSECommerceServiceBinding" contract="WebServices.Amazon.AWSECommerceServicePortType" name="AWSECommerceServicePort" />
</client>
</system.serviceModel>
Hey Brian, I'm dealing with the same issue in my app. I'm using the WSDL generated code -- in fact I generated it again today to ensure the latest version. I found that signing with an X509 certificate the most straightforward path. With a few minutes of testing under my belt, so far it appears to work okay. Essentially you change from:
AWSECommerceService service = new AWSECommerceService();
// ...then invoke some AWS call
To:
AWSECommerceService service = new AWSECommerceService();
service.ClientCertificates.Add(X509Certificate.CreateFromCertFile(#"path/to/cert.pem"));
// ...then invoke some AWS call
Viper at bytesblocks.com posted more details, including how to obtain the X509 certificate Amazon generates for you.
EDIT: as the discussion here indicates, this might not actually sign the request. Will post as I learn more.
EDIT: this doesn't appear to sign the request at all. Instead, it appears to require an https connection, and uses the certificate for SSL client authentication. SSL client authentication is an infrequently used feature of SSL. It would have been nice if the Amazon product advertising API supported it as an authentication mechanism! Unfortunately that doesn't seem to be the case. The evidence is twofold: (1) it's not one of the documented authentication schemes, and (2) it doesn't matter what certificate you specify.
Some confusion is added by Amazon still not enforcing authentication on requests even after their proclaimed the August 15 2009 deadline. This makes requests appear to pass correctly when the certificate is added, even though it might not add any value.
Look at Brian Surowiec's answer for a solution that works. I'm leaving this answer here to document the appealing but apparently failed approach, as I can still see it discussed in blogs and Amazon forums.
You can do this using the ProtectionLevel attributes. See Understanding Protection Level.
The soap implementation of the signature is kindof nasty. I did it in PHP for use on http://www.apisigning.com/. The trick that I finally figured out was that the Signature, AWSAccessKey, and Timestamp parameters need to go in the SOAP header. Also, the Signature is just a hash of the Operation + timestamp, and doesn't need to include any parameters.
I'm not sure how that fits into C#, but thought it might be of some use

Categories

Resources