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.

Bad User Experience: Country-Specific Media Embeds

More and more, there appear to be cracks appearing in the unified facade of media on the Internet – and this is not a good thing. The example which irks me the most is when traditional media hamfistedly attempts to put content online, and then erects artificial barriers to viewing that content based on the user’s geographic location. A perfect example: the behaviour of Comedy Central’s embedded videos when viewed from Canada.

If you’re cruising through the Internet from Canada and come across an embedded Comedy Central video, this is what you see:

Available on the Internet! *except in Canada

Available on the Internet! *except in Canada

I imagine the conversation at Comedy Central went something like this:

  1. “Hey! Let’s put our content online to drive awareness of our programs!”
  2. “And you know what would be cool? What if we let people embed our content on other sites, essentially advertising our programs to new audiences for free!”
  3. “Hmm, wait a minute. We’d better make sure we don’t piss off partners in other countries that license our content – let’s prevent people in other countries from viewing that embedded content.”

I understand the logic of #3 – Comedy Central licenses its content to other media players in other countries (in Canada, it’s the Comedy Network – a near-verbatim copy of Comedy Central). Of course, those licensees want people to understand that in certain geographies, the content is available from them, not Comedy Central. The problem is that #3 completely undermines #1 and #2; as implemented, the licensee completely fails to capitalize on the distributed, border-less nature of the Internet.

Instead of gaining a follower, this behavior simple annoys the user – after all, who really says to themselves “Oh, I get it – I guess I’ll just navigate to the Comedy Network and hunt down this clip to see it“? Nobody, that’s who. Instead,  the user says to themselves: “Wow, these guys are idiots because they don’t let me see the video. Oh well, luckily there’s a whole bunch of other stuff on the Internet. I’ll just go and watch those things instead.”

All it does is annoy the very audience the company is trying to reach:

Dear Comedy Central: Stop Being Stupid

The funny part here is that there is a simple solution: simply brand the player interface depending on the user’s location. If the user is from the US, brand it as “Comedy Central”; if it’s in Canada, brand it as “The Comedy Network”. If the user clicks on the embed, take them to the right site for their country. Easy.

My advice for media companies: as you venture online, ask yourself how you can interest as many people as possible in your content, and make the country-specific licensee issues as transparent to the user as possible.