Duplicate security elements when building custom soap request - c#

I'm trying to send a soap request to a WCF service. I am building the soap request using the System.ServiceModel.Channels.Message.CreateMessage() method.
I haven't gotten super deep into building the body, but here is what I have...
Message msg = Message.CreateMessage( MessageVersion.Soap11WSAddressing10, "MethodName" );
msg.Headers.MessageId = new UniqueId( Guid.NewGuid().ToString() );
msg.Headers.Add( Message.CreateHeader( "Security", "",
new Security()
{
TimeStamp = new TimeStampType() {
Created = DateTime.Now,
Expires = Created.AddDays( 1 )
},
UsernameToken = new UsernameToken() {
Username = "stackoverflow",
Password = new Password() {
Type = "hashed",
Value = "Password"
}
}
}, new SecuritySerializer() ) ) ); // The serializer inherits XmlObjectSerializer. Internally it is using the XmlSerializer class.
string s = msg.ToString();
When I run this, I get the following output. I'm using the Visual Studio XML Visualizer btw.
<s:Envelope>
<s:Header>
<Action>MethodName</Action>
<MessageID>GUIDVALUE</MessageID>
<Security>
<Security>
.....
</Security>
</Security>
</s:Header>
<s:Body />
</s:Envelope>
My question is, how can I remove one of the Security elements? I'm supposed to have one, but I have two since I first passed in the element name to the Message.CreateHeader() method and the second one was generated from the serializer.
Thanks in advance

I fixed this by making the Security class inherit from the MessageHeader abstract class. I then overrided the OnWriteHeaderContents method and serialized the properties there. Also I took out MessageHeader.CreateHeader() since the Security object is now a header object.

Related

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.

Soap request and response in c#

I wrote c# code for a Soap XML request, I have verified this code generating an XML class.
My question is how to send request and receive response using c# code.
Please be kind with my simple or basic mistakes because I am a newbie to XML but your help would be really appreciated.
SOAP XML Request:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ce="http://www." xmlns:os="http://www.domainname.com/schema/soap/v1" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<soapenv:Header />
<soapenv:Body>
<ce:message>
<ce:m_control>
<os:control_timestamp>2001-12-31T12:00:00</os:control_timestamp>
<os:message_id>000000000000000000000000000000000</os:message_id>
<os:message_type>Contract Enquiry Request</os:message_type>
<os:message_version>ce/v2.2/NameContractRequest</os:message_version>
<os:expected_response_type>synchronous</os:expected_response_type>
<os:initiator_id>initiator_id</os:initiator_id>
<os:initiator_orchestration_id>initiator_orchestration_id</os:initiator_orchestration_id>
<os:KeyInfo>
<ds:X509Data>
<ds:X509IssuerSerial>
<ds:X509IssuerName>CN=OSIS Customer CA, O=Origo Secure Internet Services Ltd., CN=OSIS Customer CA</ds:X509IssuerName>
<ds:X509SerialNumber>111111111111111111111111111111111111</ds:X509SerialNumber>
</ds:X509IssuerSerial>
<ds:X509SubjectName>C=GB, O=FirmID3400010000023NR11QQ, OU=CPS - www.unipass.co.uk/cps, OU=Warning/Terms of Use - www.unipass.co.uk/tou, OU=EmployeeID10101101010101, OU=TPSP2, OU=BPNR1 1QQ, CN=Testt Orgg/emailAddress=Fname.Lname#aviva.co.uk</ds:X509SubjectName>
</ds:X509Data>
</os:KeyInfo>
<os:responder_id>os:responder_id</os:responder_id>
</ce:m_control>
<ce:m_content>
<ce:b_control>
<ce:contract_enquiry_reference>TestRequest</ce:contract_enquiry_reference>
</ce:b_control>
<ce:intermediary>
<ce:FirmFSARef id="idvalue14">456123</ce:FirmFSARef>
</ce:intermediary>
<ce:request_scope>
<ce:contract_details_required_ind>No</ce:contract_details_required_ind>
<ce:valuation_currency>GBP</ce:valuation_currency>
<ce:fund_code_type_required>SEDOL</ce:fund_code_type_required>
<ce:valuation_request ce:type="Current" />
</ce:request_scope>
<ce:contract>
<ce:contract_reference_number>TL12345678</ce:contract_reference_number>
</ce:contract>
</ce:m_content>
</ce:message>
</soapenv:Body>
</soapenv:Envelope>
################################################ c# code ##################
Guid CEGuid = Guid.NewGuid();
string GuidString = CEGuid.ToString();
string CEVersion = "";
string URL = "";
string ResponderId = "";
string ContractDetailsRequired = "Yes";
using (XmlTextWriter xmlRequestWriter = new XmlTextWriter(#"C:/Unipass/PensionRequest.xml", Encoding.UTF8))
{
xmlRequestWriter.WriteStartDocument();
xmlRequestWriter.WriteComment("This file is generated by the program.");
xmlRequestWriter.WriteStartElement("soapenv:Envelope");
xmlRequestWriter.WriteAttributeString("xmlns:soapenv", null, "http://schemas.xmlsoap.org/soap/envelope/");
xmlRequestWriter.WriteAttributeString("xmlns:ce", null, "http://www.WhateveryDomain.com/schema/ce/v2.2/WhateverRequest");
xmlRequestWriter.WriteAttributeString("xmlns:os", null, "http://www.WhateveryDomain.com/schema/soap/v1");
xmlRequestWriter.WriteAttributeString("xmlns:ds", null, "http://www.w3.org/2000/09/xmldsig#");
xmlRequestWriter.WriteStartElement("soapenv:Header");
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteStartElement("ce:message");
xmlRequestWriter.WriteStartElement("ce:m_control");
xmlRequestWriter.WriteElementString("os:control_timestamp", DateTime.Now.ToString("s"));
xmlRequestWriter.WriteElementString("os:message_id", GuidString);
xmlRequestWriter.WriteElementString("os:message_type", "Contract Enquiry Request");
xmlRequestWriter.WriteElementString("os:message_version", "ce/v2.2/WhateverRequest");
xmlRequestWriter.WriteElementString("os:expected_response_type", "synchronous");
xmlRequestWriter.WriteElementString("os:initiator_id", "initiator_id");
xmlRequestWriter.WriteElementString("os:initiator_orchestration_id", "initiator_orchestration_id");
xmlRequestWriter.WriteStartElement("os:KeyInfo");
xmlRequestWriter.WriteStartElement("ds:X509Data");
xmlRequestWriter.WriteStartElement("ds:X509IssuerSerial");
xmlRequestWriter.WriteElementString("ds:X509IssuerName", "CN=OSIS Customer CA, O=Origo Secure Internet Services Ltd., CN=OSIS Customer CA");
xmlRequestWriter.WriteElementString("ds:X509SerialNumber", "111111111111111111111111111111111");
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteElementString("ds:X509SubjectName", "C=GB, O=FirmID3400010000023NR11QQ, OU=CPS - www.unipass.co.uk/cps, OU=Warning/Terms of Use - www.unipass.co.uk/tou, OU=EmployeeID01200012000003, OU=TPSP2, OU=BPNR1 1QQ, CN=Testt Orgg/emailAddress=fname.lastname#aviva.co.uk");
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteElementString("os:responder_id", "os:responder_id");
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteStartElement("ce:m_content");
xmlRequestWriter.WriteStartElement("ce:b_control");
xmlRequestWriter.WriteElementString("ce:contract_enquiry_reference", "TestRequest");
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteStartElement("ce:intermediary");
xmlRequestWriter.WriteStartElement("ce:FirmFSARef");
xmlRequestWriter.WriteElementString("id", "456123");//="idvalue14">
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteStartElement("ce:request_scope");
xmlRequestWriter.WriteElementString("ce:contract_details_required_ind", "Yes");
xmlRequestWriter.WriteElementString("ce:valuation_currency", "GBP");
xmlRequestWriter.WriteElementString("ce:fund_code_type_required", "SEDOL");
xmlRequestWriter.WriteStartElement("ce:valuation_request");
xmlRequestWriter.WriteElementString("ce:type", "Current");//"ce:type","Current"
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteStartElement("ce:contract");
xmlRequestWriter.WriteElementString("ce:contract_reference_number", "PP12345678");
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteEndDocument();
xmlRequestWriter.Flush();
Please could you tell me how to send Request and receive response from WCF service using above code.
Even though I have added wsdl reference but I don't know how to pass XmlElement[] in the line below.
serviceReference.getDetail(ref XmlElement[] Any);
Here is a method in Reference class.
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(Namespace="http://www.domainname.com/whatever/webname/schema/ce/v2.2/NameContractServic" +
"e", ConfigurationName="TestingPension.NameContractServiceDetailType")]
public interface NameContractServiceDetailType {
// CODEGEN: Generating message contract since the wrapper namespace (http://www.domainname.com/schema/ce/v2.2/NameContractRequest) of message getDetailRequest does not match the default value (http://www.domainname.com/name/name/schema/ce/v2.2/NameContractService)
[System.ServiceModel.OperationContractAttribute(Action="http://www.origostandards.com/schema/ce/v2.2/CEPensionSingleContract#getDetail", ReplyAction="*")]
[System.ServiceModel.FaultContractAttribute(typeof(SoapReqResWebApplication.TestingPension.Error[]), Action="http://www.domainname.com/schema/ce/v2.2/NameContract#getDetail", Name="errors", Namespace="http://www.domainname.com/schema/tech/v1.0/SOAPFaultDetail")]
[System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults=true)]
SoapReqResWebApplication.TestingPension.getDetailResponse getDetail(SoapReqResWebApplication.TestingPension.getDetailRequest request);
}
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
[System.ServiceModel.MessageContractAttribute(WrapperName="message", WrapperNamespace="http://www.domainname.com/schema/ce/v2.2/NameContractRequest", IsWrapped=true)]
public partial class getDetailRequest {
[System.ServiceModel.MessageBodyMemberAttribute(Namespace="", Order=0)]
[System.Xml.Serialization.XmlAnyElementAttribute()]
public System.Xml.XmlElement[] Any;
public getDetailRequest() {
}
public getDetailRequest(System.Xml.XmlElement[] Any) {
this.Any = Any;
}
}
#CodeCaster
I tried your code as:
var documentToSend = new XmlDocument();
//// TODO: add all elements you like
using (XmlTextWriter xmlRequestWriter = new XmlTextWriter(#"C:/Unipass/Request.xml", Encoding.UTF8))
{
xmlRequestWriter.WriteStartDocument();
xmlRequestWriter.WriteStartElement("ce:message");
xmlRequestWriter.WriteEndElement();
xmlRequestWriter.WriteEndDocument();
xmlRequestWriter.Flush();
documentToSend.Save(xmlRequestWriter);
//// Create an array with the root message node as only element.
var xmlToSend = new XmlElement[] { documentToSend.DocumentElement }; //xmlToSend = null
}
Might be I am doing completely wrong, please can you tell me why xmlToSend is null.
It seems that you are writing code by hand to serialise messages into SOAP/XML. The SOAP request example is an actual instance document rather than WSDL, yes? This is one of the things that WCF does for you - you should not need to generate your own SOAP messages.
In WCF, you focus on the object types you want to transmit and the operations you want to support. Then WCF does all the heavy lifting of SOAP serialisation for you. That is the main point of WCF really.
It's quite hard to decipher from your sample SOAP message what exactly you are trying to do, so I would recommend looking at one of the simple WCF samples, say from here
http://msdn.microsoft.com/en-us/library/vstudio/ms751514(v=vs.90).aspx
Get a simple one up and running using wsHttpBinding or basicHttpBinding (these are SOAP bindings in WCF). Then capture the HTTP request using Fiddler or similar and see what WCF has done for you in terms of the SOAP envelope generation.
You should then be able to translate that to your own case.
It seems like WCF's proxy generator cannot create a class from the provided WSDL/XSD, given the generated method signature is getDetailRequest(System.Xml.XmlElement[] Any). What does a tool like SoapUI say about the WSDL?
A way to solve this is to indeed manually generate the message, which is just the <ce:message>..</ce:message> block. WCF will wrap it in a proper SOAP envelope when you call the service method.
You'll have to play around a bit with the XmlDocument class, but I think something like this will do it:
// Create an XmlDocument and fill it
var documentToSend = new XmlDocument();
// TODO: add all elements you like
// Create an array with the root message node as only element.
var xmlToSend = new XmlElement[] { documentToSend.DocumentElement };
// Call the service.
var response = serviceReference.getDetail(xmlToSend);
Another way would be to manually recreate the request class in C# and populate and serialize an instance of it to XML when sending a request. The ultimate way would be to fix the XML so Add Service Reference (SvcUtil) can generate classes from the service's metadata.
As for your edit, you're not writing the XML elements to the documentToSend, but you write them to your file.

Can I remove the default namespace when creating raw Soap XML?

I'm trying to send a soap request to a WCF service. I am building the soap request using the System.ServiceModel.Channels.Message.CreateMessage() method.
I haven't gotten super deep into building the body, but here is what I have...
Message msg = Message.CreateMessage( MessageVersion.Soap11WSAddressing10, "MethodName" );
msg.Headers.MessageId = new UniqueId( Guid.NewGuid().ToString() );
msg.Headers.Add( Message.CreateHeader( "Security", "",
new Security()
{
TimeStamp = new TimeStampType() {
Created = DateTime.Now,
Expires = Created.AddDays( 1 )
},
UsernameToken = new UsernameToken() {
Username = "stackoverflow",
Password = new Password() {
Type = "hashed",
Value = "Password"
}
}
} ) ) );
string s = msg.ToString();
When I run this, I get the following output. I'm using the Visual Studio XML Visualizer btw.
<s:Envelope>
<s:Header>
<Action>MethodName</Action>
<MessageID>GUIDVALUE</MessageID>
<Security>
<Timestamp xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1">
.....
</s:Header>
<s:Body />
</s:Envelope>
My question is, can I remove xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1" from the xml? It shows up in Timestamp and in UsernameToken.
Thanks
set Namespace to empty in datacontract
in a class that wrap your security and timestamp
[DataContract(Namespace = "")]

Can't pass java parameters in the same format as c# for WCF service

This is the scenario: I have a WCF service running, who communicates with this method, in C#:
public bool ValidateUser(UserPass up)
{
initializeAttributes();
IMembershipService Member = new AccountMembershipService();
bool login = Member.ValidateUser(up.User, up.Pass);
return login;
}
The parameter are encapsulated in this class:
[DataContract]
public class UserPass
{
string user = "";
string pass = "";
string email = "";
[DataMember]
public string User
{
get { return user; }
set { user = value; }
}
[DataMember]
public string Pass
{
get { return pass; }
set { pass = value; }
}
[DataMember]
public string Email
{
get { return email; }
set { email = value; }
}
}
Now, I want to connect to the server via an Android application, now, my question is, how can I replicate the UserPass class in Java, so the ValidateUser method can receive its parameter in a way it can understands it.
for reference, this is the code where I'm obtaining the User and Password:
private void validateUser(String user, String pass)
{
String SOAP_ACTION = "http://tempuri.org/IUserService/ValidateUser/";
String METHOD_NAME = "ValidateUser";
String NAMESPACE = "http://tempuri.org/";
String URL = "http://10.0.2.2/UserService.svc";
AlertDialog popup;
SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME);
request.addProperty(user, pass);
SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
envelope.bodyOut = request;
envelope.dotNet = true;
HttpTransportSE httpTransport = new HttpTransportSE(URL);
try
{
httpTransport.call(SOAP_ACTION, envelope); //here's the exception!!
Object response = envelope.getResponse();
popup = createAlertDialog("Respuesta",response.toString(),"OK");
popup.show();
}
catch (Exception exception)
{
String exceptionStr=exception.toString();
popup = createAlertDialog("Exception!!",exceptionStr,"OK");
popup.show();
}
}
The exception it throws is xmlpullparserexception, which, according to my understanding, is because of a missmatch between the parameters of the request and the actual method.
Many thanks for reading my question, and many more for those who can answer it :)
EDIT:
I finnaly got how to compare the XMLs... now, this is what my SOAP is providing:
<v:Envelope xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:d="http://www.w3.org/2001/XMLSchema"
xmlns:c="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:v="http://schemas.xmlsoap.org/soap/envelope/">
<v:Header />
<v:Body>
<ValidateUser xmlns="http://tempuri.org/" id="o0" c:root="1">
<User i:type="d:string">someuser</User>
<Pass i:type="d:string">somepass</Pass>
<Email i:type="d:string"></Email>
</ValidateUser>
</v:Body>
and this is what it SHOULD have made (retrieved from WCF Test Client application from Visual Studio 2010):
<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">http://tempuri.org/IUserService/ValidateUser</Action>
</s:Header>
<s:Body>
<ValidateUser xmlns="http://tempuri.org/">
<up xmlns:d4p1="http://schemas.datacontract.org/2004/07/LiveAndesWCF" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<d4p1:Email i:nil="true" />
<d4p1:Pass>somepass</d4p1:Pass>
<d4p1:User>someuser</d4p1:User>
</up>
</ValidateUser>
</s:Body>
</s:Envelope>
Now, I'm lost on how to code my soap code to have it generate a xml file like the latter one.
Many thanks again.
have you tried to look at the xml created by the soap call? you can compare it to the xml created by a .net proxy. maybe this helps to find a solution.
here is how you can enable the logging of the soap calls:
http://msdn.microsoft.com/en-us/library/ms730064.aspx
This line looks suspect to me:
request.addProperty(user, pass);
As far as I can tell, SoapObject comes from the KSOAP2 library, and according to the docs, addProperty takes the name of the property and the value. To set user and pass, I would expect something more like this:
request.addProperty("user", user);
request.addProperty("pass", pass);
Currently, it looks like you're adding a single property named using the value of the user parameter. If the endpoint is expecting at least 2 arguments, then this could be the source of your mismatch.
Also, is the value "Email", from the UserPass wrapper class, optional? As I don't see it being set anywhere, and the wrapper class suggests it's required by the SOAP request

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