Signing Amazon Web Service Requests in ActionScript
Amazon.com announced a change to its Product Advertising API requiring all requests after August 15, 2009 to be signed. I’d been meaning to update one of the Scannerfly example applications and the Shelfari Scanner to sign Amazon requests, but was hoping someone else would figure it out before me. As near as I can tell, no one has provided an implementation in ActionScript, so I cobbled one together.
Generally, the process is fairly straighforward: developers need to transform their REST request’s parameters into a canonical form, add a timestamp, and then append a signed version of the resulting string. However, in reality there a lots of finicky details that make the process frustrating…luckily for you, I’ve created an ActionScript implementation that you should be able to modify to suit your purposes.
The signature process relies on the as3crypto library to provide HMAC and SHA-256 implementations. In addition, you’ll need an instance of mx.rpc.http.HttpService
that you’ve likely instantiated in your MXML, along with your Amazon Web Services developer ID and secret key:
// A HTTPService configured to perform the required Amazon Web Services request. <mx:HTTPService id="AmazonSearch" url="http://webservices.amazon.com/onca/xml" showBusyCursor="true"> <mx:request> <awsaccesskeyid>{amazonDeveloperId}</awsaccesskeyid> <idtype>EAN</idtype> <itemid>9781592400874</itemid> <operation>ItemLookup</operation> <responsegroup>ItemAttributes,Images,Tracks,EditorialReview</responsegroup> <searchindex>Books</searchindex> <service>AWSECommerceService</service> <signature>{signature}</signature> <timestamp>{timestamp}</timestamp> </mx:request> <mx:HTTPService> // The Amazon host providing the Product API web service. private const AWS_HOST:String = "webservices.amazon.com"; // The HTTP method used to send the request. private const AWS_METHOD:String = "GET"; // The path to the Product API web service on the Amazon host. private const AWS_PATH:String = "/onca/xml"; // The AWS Access Key ID to use when querying Amazon.com. [Bindable] private var amazonDeveloperId:String = "####################"; // The AWS Secret Key to use when querying Amazon.com. [Bindable] private var amazonSecretAccessKey:String = "####################"; // The request signature string. [Bindable] private var signature:String; // The request timestamp string, in UTC format (YYYY-MM-DDThh:mm:ssZ). [Bindable] private var timestamp:String; private function generateSignature():void { var parameterArray:Array = new Array(); var parameterCollection:ArrayCollection = new ArrayCollection(); var parameterString:String = ""; var sort:Sort = new Sort(); var hmac:HMAC = new HMAC(new SHA256()); var requestBytes:ByteArray = new ByteArray(); var keyBytes:ByteArray = new ByteArray(); var hmacBytes:ByteArray; var encoder:Base64Encoder = new Base64Encoder(); var formatter:DateFormatter = new DateFormatter(); var now:Date = new Date(); // Set the request timestamp using the format: YYYY-MM-DDThh:mm:ss.000Z // Note that we must convert to GMT. formatter.formatString = "YYYY-MM-DDTHH:NN:SS.000Z"; now.setTime(now.getTime() + (now.getTimezoneOffset() * 60 * 1000)); timestamp = formatter.format(now); // Process the parameters. for (var key:String in AmazonSearch.request ) { // Ignore the "Signature" request parameter. if (key != "Signature") { var urlEncodedKey:String = encodeURIComponent(decodeURIComponent(key)); var parameterBytes:ByteArray = new ByteArray(); var valueBytes:ByteArray = new ByteArray(); var value:String = AmazonSearch.request[key]; var urlEncodedValue:String = encodeURIComponent(decodeURIComponent(value.replace(/\+/g, "%20"))); // Use the byte values, not the string values. parameterBytes.writeUTFBytes(urlEncodedKey); valueBytes.writeUTFBytes(urlEncodedValue); parameterCollection.addItem( { parameter : parameterBytes , value : valueBytes } ); } } // Sort the parameters and formulate the parameter string to be signed. parameterCollection.sort = sort; sort.fields = [ new SortField("parameter", true), new SortField("value", true) ]; parameterCollection.refresh(); parameterString = AWS_METHOD + "\n" + AWS_HOST + "\n" + AWS_PATH + "\n"; for (var i:Number = 0; i < parameterCollection.length; i++) { var pair:Object = parameterCollection.getItemAt(i); parameterString += pair.parameter + "=" + pair.value; if (i < parameterCollection.length - 1) parameterString += "&"; } // Sign the parameter string to generate the request signature. requestBytes.writeUTFBytes(parameterString); keyBytes.writeUTFBytes(amazonSecretAccessKey); hmacBytes = hmac.compute(keyBytes, requestBytes); encoder.encodeBytes(hmacBytes); signature = encodeURIComponent(encoder.toString()); } ... // Somewhere in your code you'll call the following to generate request signature and perform the search. generateSignature(); AmazonSearch.send(); |
And for those who need a complete working example, you can download the MXML for an example application from here. The example simply performs an ItemLookup; however, you will still need to add your Amazon developer ID and secret key for the example to work.
A note to other developers struggling with implementing the proper request signature, see the Signed Request Helper. This Javascript application breaks down each step in the formulating the normalized parameter string and signature.
Is this for Flex? I’m searching for some help on a as3 flash version without much luck.
Of course, it would be a terrible idea to distribute your secret key in a publicly visible swf. As such, any signing process should remain secret. This means that the signing should really happen server-side.
@de-lin: Yes, this is for Flex. However, you should be able to use the function as-is to generate the signature for your request. I believe you have access to the HttpService class from Flash as well.
@spender: Yes and no. Yes, I agree that distributing the key in a pubicly visible SWF would be a bad idea. However, there are still cases where you might use client-side signing – in particular, I could see it as being reasonable for a Flex app deployed on a stand-alone kiosk. As with all technology, it’s the developer’s responsibility to decide what is or isn’t appropriate for their application.
I don’t believe your MXML example works on Flex 3. Have you tried it?
Yes, it works. However, there were some constants and variables that I omitted from the code in the name of brevity. However, given that those variables and constants are necessary to make it work, I have re-added them to the example code above.
Thanks very much for doing this! It worked well for me.
Hi Brendon,
thanks a lot for your effort! It saved me many hours of coding and headaches.
Just mentioning that for me the behaviour was rather erratic: it would work sometimes, and other times Amazon would say it doesn’t like the signature. By the way, I am using FLex SDK 4. So I investigated the issue a bit and I found that whenever Amazon said it didn’t like the signature, it contained special characters like + or =. So, for example, the signature I was sending it was 4+ay2iDrJqhmMqK58uKdidh7pLMNYFpAd0GuC4NWB+I= , but it was expecting 4%2Bay2iDrJqhmMqK58uKdidh7pLMNYFpAd0GuC4NWB%2BI%3D. What I did was to change the line “signature = encodeURI(encoder.toString());” into “signature = encodeURIComponent(encoder.toString());”, because encodeURIComponent encodes many more characters than encodeURI – and Amazon mention here http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/Query_QueryAuth.html, at the bottom of the page, that the signature itself needs to be encoded as per RFC3986. It workes perfectly now. And by the way, I looked at the SDK 3 documentation, and nothing changed in the functionality of the encodeURIComponent function, so I am surprised all of you guys didn’t have any problems.
And just touching on that one, the ActionScript implementation of encodeURIComponent actually encodes fewer characters than Amazon would like. But it does seem to work fine, so it may be that those particular characters don’t appear in the signature at all. But you know better 😉
Again, fantastic work! Thanks.
Mihai
Mihal – thanks for the update to the code! I had used encodeURIComponent earlier in the code, but had failed to use it when generating the final signature. Whoops! I’ve updated the code above to reflect your suggestion.
Thanks you very much from France!
I tried using the corelib:
trace (HMAC.hash(key,data,SHA256));
But it doesnt work ;-(
Tom – The problem probably isn’t your use of corelib. Amazon is pretty particular about the encoding of the data (see the use of encodeURIComponent in the code above), so I would guess that was the source of your problem.
Yep right, I cannot understand why Amazon decided to add this fu*** security feature! Now the web service to get albums art is (alsmot) more secure than a paypall service 😉
Thanks a bunch for posting this code. It still works :).
Are there licensing issues around use of your example code? We plan to use it in another example (reference) application. Please advise… and thanks for posting the code, it works!
Kirsten – the code is here for you to use as you see fit. Go crazy.
I’ll post another version with a license bundle (probably MIT or BSD license) when I get the chance.
Hi Brendon,
Someone brought to my attention a bug in the code above. There is 1 hour everyday when the script stops working and Amazon returns an “invalid date” error. It is UTC 24. The data format string uses the HH code for 1-24 while Amazon is expecting JJ for 0-23.
Fixed string:
formatter.formatString = “YYYY-MM-DDTJJ:NN:SS.000Z”;
–jason
Hi Brendon,
your “signaturegenerator” works perfect, but i have a problem with excuting the httpservice.
Triggering the Amazonsearch.send() returns the errormessage: “HTTP request error”
The only thing I have changed is “upgrading” your source from Flex 3 to Flex 4. It doesn’t matter if i use <mx:httpservice and <mx:request or the spark equivalent. the error is the same.
The Signature-Part is working fine.
Bringing the elements with cut & paste and a little modification together: "http://" + endpoint-url +"?" + request-values + &Signature returns after copying the url and executing it in a browser the expected result. That is great!!!!
Have I done something wrong when I make a flex 4 app out of your mxml-file? Changing <mx:httpService and <mx:request to spark makes no difference.
Is something in my crossdomain.xml wrong and do I need this file for this httpservice-connection?
Her comes my less modified version of your example.
Look at the alert-boxes. All values are correct!
{amazonDeveloperId}
EAN
9783934358058
ItemLookup
ItemAttributes,Images,Tracks,EditorialReview
Books
AWSECommerceService
{signature}
{timestamp}
<![CDATA[
import com.hurlant.crypto.hash.HMAC;
import com.hurlant.crypto.hash.SHA256;
import mx.collections.ArrayCollection;
import mx.collections.Sort;
import mx.collections.SortField;
import mx.controls.Alert;
import mx.formatters.DateFormatter;
import mx.rpc.events.ResultEvent;
import mx.rpc.http.HTTPService;
import mx.utils.Base64Encoder;
/*
//Changes:
url="http://ecs.amazonaws.de/onca/xml"
…
url=”http://webservices.amazon.com/onca/xml”
AWS_HOST:String = “webservices.amazon.com”;
German Endpoint:
url=”http://ecs.amazonaws.de/onca/xml”
AWS_HOST:String = “ecs.amazonaws.de”;
*/
/*
Ablauf der Signatur aus : http://associates-amazon.s3.amazonaws.com/signed-requests/helper/index.html
Das folgende Beispiel wurde getestet. Es handelt sich um das Flash4 Buch von S. Wolter
String-To-Sign wird hier aus den Parametern aufgebaut:
Hinweis: Die Signatur ist noch nicht enthalten, das sie nun aus diesen Werten erzeugt wird!
GET
ecs.amazonaws.de
/onca/xml
AWSAccessKeyId=***MYKEY***&
AssociateTag=mytag-20&
IdType=ISBN&
ItemId=3924322112&
Operation=ItemLookup&
ResponseGroup=ItemAttributes%2COffers%2CImages%2CReviews%2CVariations&
SearchIndex=Books&
Service=AWSECommerceService&
Sort=salesrank&
Timestamp=2011-05-19T13%3A17%3A27.000Z&
Version=2009-01-01
daraus ergibt sich die Signed URL:
http://ecs.amazonaws.de/onca/xml?
AWSAccessKeyId=***MYKEY***&
AssociateTag=mytag-20&
IdType=ISBN&
ItemId=3924322112&
Operation=ItemLookup&
ResponseGroup=ItemAttributes%2COffers%2CImages%2CReviews%2CVariations&
SearchIndex=Books&
Service=AWSECommerceService&
Sort=salesrank&
Timestamp=2011-05-19T13%3A17%3A27.000Z&
Version=2009-01-01&
Signature=plM4oRhUyxSZumgQ7q7X6GsbGGWpvVjCch1eV5K%2B7ps%3D
—————————————————————
mit diesem Programm wurde erzeugt:
Hinweis die Parameter weichen etwas von den App-Parametern ab
GET
ecs.amazonaws.de
/onca/xml
AWSAccessKeyId=***MYKEY***&
IdType=EAN&
ItemId=9783934358058&
Operation=ItemLookup&
ResponseGroup=ItemAttributes%2CImages%2CTracks%2CEditorialReview&
SearchIndex=Books&
Service=AWSECommerceService&
Timestamp=2011-05-19T14%3A45%3A41.000Z
Manuell zusammengestetzt funktioniert der REST-Request:
Es wurde “http://”, das “?” hinter xml und “&Signature=f5km6UtycJVjUsxfpFbxZfcWVaS0CmziR7tBATHdCJE%3D” hinzugefuegt:
Bitte beachten: Die Signatur wurde OHNE diese Ergaenzungen erzeugt !!!
http://ecs.amazonaws.de/onca/xml?
AWSAccessKeyId=***MYKEY***&
IdType=EAN&ItemId=9783934358058&
Operation=ItemLookup&
ResponseGroup=ItemAttributes%2CImages%2CTracks%2CEditorialReview&
SearchIndex=Books&
Service=AWSECommerceService&
Timestamp=2011-05-19T14%3A58%3A15.000Z&
Signature=f5km6UtycJVjUsxfpFbxZfcWVaS0CmziR7tBATHdCJE%3D
*/
/**
* The Amazon host providing the Product API web service.
*/
//private const AWS_HOST:String = “ecs.amazonaws.de”;
private const AWS_HOST:String = “webservices.amazon.com”;
/**
* The HTTP method used to send the request.
*/
private const AWS_METHOD:String = “GET”;
/**
* The path to the Product API web service on the Amazon host.
*/
private const AWS_PATH:String = “/onca/xml”;
/**
* The AWS Access Key ID to use when querying Amazon.com.
*/
[Bindable]
private var amazonDeveloperId:String = “***MYKEY***”;
/**
* The AWS Secret Key to use when querying Amazon.com.
*/
[Bindable]
private var amazonSecretAccessKey:String = “***MYSECRETKEY***”;
/**
* The request signature string.
*/
[Bindable]
private var signature:String;
/**
* The request timestamp string, in UTC format (YYYY-MM-DDThh:mm:ssZ).
*/
[Bindable]
private var timestamp:String;
/**
* Calls all of the registered Javascript callback functions with
* the details on the barcodes that have been detected.
*/
private function init():void
{
// Generate request signature and perform the search.
generateSignature();
//mx.controls.Alert.show(“amazonDeveloperId:” + amazonDeveloperId);
mx.controls.Alert.show(“&signature=” + signature);
mx.controls.Alert.show(“×tamp=” + timestamp);
var req:String = “”;
req += “AWSAccessKeyId =” + this.AmazonSearch.request.AWSAccessKeyId + “\n”;
req += “IdType =” + this.AmazonSearch.request.IdType+ “\n”;
req += “ItemId =” + this.AmazonSearch.request.ItemId+ “\n”;
req += “Operation =” + this.AmazonSearch.request.Operation+ “\n”;
req += “ResponseGroup =” + this.AmazonSearch.request.ResponseGroup+ “\n”;
req += “SearchIndex =” + this.AmazonSearch.request.SearchIndex+ “\n”;
req += “Service =” + this.AmazonSearch.request.Service+ “\n”;
req += “Signature =” + this.AmazonSearch.request.Signature+ “\n”;
req += “Timestamp =” + this.AmazonSearch.request.Timestamp+ “\n”;
Alert.show(“requstValues:\n” + req);
}
/**
* Displays the dialog box with the details retrieved from Amazon.
* This function is called by the AmazonSearch object’s ItemLookup
* operation. Note that scanning is paused while the dialog box
* is being displayed.
*
* @param res The results of the query to Amazon.com.
*/
private function showItemLookupResults(res:ResultEvent):void
{
var details:Object = res.result.ItemLookupResponse.Items.Item;
if (details != null)
{
// TODO: Do something with the result of the request.
Alert.show(“detailResults” + details);
}
}
/**
* Handles generating the signature for the AWS request. See the
* request authentication process details in the documentation for
* Amazon’s Product API, available at:
*
* http://docs.amazonwebservices.com/AWSECommerceService/2009-07-01/DG/HMACSignatures.html
*
* There are also examples of the necessary canonicalization at:
*
* http://docs.amazonwebservices.com/AWSECommerceService/2009-07-01/DG/rest-signature.html
*
* @return The base64-encoded Amazon request signature.
*/
private function generateSignature():void
{
var parameterArray:Array = new Array();
var parameterCollection:ArrayCollection = new ArrayCollection();
var parameterString:String = “”;
var sort:Sort = new Sort();
var hmac:HMAC = new HMAC(new SHA256());
var requestBytes:ByteArray = new ByteArray();
var keyBytes:ByteArray = new ByteArray();
var hmacBytes:ByteArray;
var encoder:Base64Encoder = new Base64Encoder();
var formatter:DateFormatter = new DateFormatter();
var now:Date = new Date();
// Set the request timestamp using the format: YYYY-MM-DDThh:mm:ss.000Z
// Note that we must convert to GMT.
//Version vor dem Bugfix: 1-24
//formatter.formatString = “YYYY-MM-DDTHH:NN:SS.000Z”;
//BUGFIX: Amazon is expecting JJ for 0-23
formatter.formatString = “YYYY-MM-DDTJJ:NN:SS.000Z”;
now.setTime(now.getTime() + (now.getTimezoneOffset() * 60 * 1000));
timestamp = formatter.format(now);
// Process the parameters.
for (var key:String in AmazonSearch.request )
{
// Ignore the “Signature” request parameter.
if (key != “Signature”)
{
var urlEncodedKey:String = encodeURIComponent(decodeURIComponent(key));
var parameterBytes:ByteArray = new ByteArray();
var valueBytes:ByteArray = new ByteArray();
var value:String = AmazonSearch.request[key];
var urlEncodedValue:String = encodeURIComponent(decodeURIComponent(value.replace(/\+/g, “%20”)));
// Use the byte values, not the string values.
parameterBytes.writeUTFBytes(urlEncodedKey);
valueBytes.writeUTFBytes(urlEncodedValue);
parameterCollection.addItem( { parameter : parameterBytes , value : valueBytes } );
}
}
// Sort the parameters and formulate the parameter string to be signed.
parameterCollection.sort = sort;
sort.fields = [ new SortField(“parameter”, true), new SortField(“value”, true) ];
parameterCollection.refresh();
parameterString = AWS_METHOD + “\n” + AWS_HOST + “\n” + AWS_PATH + “\n”;
for (var i:Number = 0; i < parameterCollection.length; i++)
{
var pair:Object = parameterCollection.getItemAt(i);
parameterString += pair.parameter + "=" + pair.value;
if (i
Hi Brandon,
please delete my first post;-)
your App is running fine even under Flex 4.
My Problem was that the “Bugfix”
from
signature = encodeURI(encoder.toString());
to
encodeURIComponent(encoder.toString());
doesn’t work.
Sorry, Mihai!
The Signature after using encodeURIComponent is to short.
Thanks again for your great work, Brandon!
Grettings from cologne/germany
Uwe