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>
	&lt;/mx:request&gt;
&lt;mx:HTTPService&gt;
 
// 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.

“Security error accessing url” in Flash 9,0,124,0

Adobe FlashIt appears the April update of Flash Player’s security policies have some implications for Amazon’s web services. Per the release information, it appears that is is no longer sufficient to have a crossdomain.xml file that contains <allow-access-from domain="*"/>:

In order for a SWF to send a header anywhere other than its own host, the origin domain of the SWF must have explicit permission from the host to which the header is being sent, in the form of a policy file. The existing policy file model will apply, with the same file locations and ActionScript APIs, but a new syntax will be required. To specify header-sending rights, use this new tag: <allow-http-request-headers-from>.

Without such an entry, whenever you use the WebService tag in MXML to access an Amazon web service, you’ll get the “security error accessing url” message. I currently have this problem with the following use of WebService tag:



...

This code functioned without issue until I updated to Flash Player 9,0,124,0. I’ve started a thread on the Amazon Web Services Developer Connection, so hopefully someone at Amazon add the appropriate <allow-http-request-headers-from domain="*"/> entry into the crossdomain.xml file at webservices.amazon.com to address this problem.

Hopefully this post saves some people a few hours of beating their heads against a wall. Unless I’m totally mistaken, and there’s a workaround that doesn’t involve using a proxy?