Search code examples
javascriptamazon-web-servicescryptojs

How to properly sign a GET request to Amazon's ItemLookup using client-side JavaScript only?


Here's what I have so far:

function sha256(stringToSign, secretKey) {
  return CryptoJS.HmacSHA256(stringToSign, secretKey);
} 

function getAmazonItemInfo(barcode) {
  
  var parameters = 
    "Service=AWSECommerceService&"
    + "AWSAccessKeyId=" + appSettings.amazon.accessKey + "&"
    + "Operation=ItemLookup&"
    + "ItemId=" + barcode
    + "&Timestamp=" + Date.now().toString();

  var stringToSign =
    "GET\n"
    + "webservices.amazon.com\n"
    + "/onca/xml\n"
    + parameters;

  var signature = "&Signature=" + encodeURIComponent(sha256(stringToSign, appSettings.amazon.secretKey));

  var amazonUrl =  
    "http://webservices.amazon.com/onca/xml?"
    + parameters
    + signature;

  // perform a GET request with amazonUrl and do other stuff

}

When executed as an HTTP GET request, the value of amazonUrl in the above code results in the following response from Amazon:

<?xml version="1.0"?> 
  <ItemLookupErrorResponse xmlns="http://ecs.amazonaws.com/doc/2005-10-05/">
    <Error>
      <Code>SignatureDoesNotMatch</Code>
      <Message>
        The request signature we calculated does not match the signature you provided. 
        Check your AWS Secret Access Key and signing method. Consult the service 
        documentation for details.
      </Message>
   </Error>
   <RequestId>[REMOVED]</RequestId>
  </ItemLookupErrorResponse>

Useful links:


Solution

  • I hacked around with your code and I got it working.

    function sha256(stringToSign, secretKey) {
      var hex = CryptoJS.HmacSHA256(stringToSign, secretKey);
      return hex.toString(CryptoJS.enc.Base64);
    } 
    
    function timestamp() {
        var date = new Date();
        var y = date.getUTCFullYear().toString();
        var m = (date.getUTCMonth() + 1).toString();
        var d = date.getUTCDate().toString();
        var h = date.getUTCHours().toString();
        var min = date.getUTCMinutes().toString();
        var s = date.getUTCSeconds().toString();
    
        if(m.length < 2) { m = "0" + m; }
        if(d.length < 2) { d = "0" + d; }
        if(h.length < 2) { h = "0" + h; }
        if(min.length < 2) { min = "0" + min; }
        if(s.length < 2) { s = "0" + s}
    
        var date = y + "-" + m + "-" + d;
        var time = h + ":" + min + ":" + s;
        return date + "T" + time + "Z";
    }
    
    function getAmazonItemInfo(barcode) {
        var PrivateKey = "";
        var PublicKey = "";
        var AssociateTag = "";
    
        var parameters = [];
        parameters.push("AWSAccessKeyId=" + PublicKey);
        parameters.push("ItemId=" + barcode);
        parameters.push("Operation=ItemLookup");
        parameters.push("Service=AWSECommerceService");
        parameters.push("Timestamp=" + encodeURIComponent(timestamp()));
        parameters.push("Version=2011-08-01");
    parameters.push("AssociateTag=" + AssociateTag);
    
        parameters.sort();
        var paramString = parameters.join('&');
    
        var signingKey = "GET\n" + "webservices.amazon.com\n" + "/onca/xml\n" + paramString
    
        var signature = sha256(signingKey,PrivateKey);
            signature = encodeURIComponent(signature);
    
        var amazonUrl =  "http://webservices.amazon.com/onca/xml?" + paramString + "&Signature=" + signature;
        console.log(amazonUrl);
    }
    

    The Header of the Javascript I used for some reference.

    <script src="hmac-sha256.js"></script>
    <script src="http://crypto-js.googlecode.com/svn/tags/3.0.2/build/components/enc-base64-min.js"></script>
    <script src="amazon.js"></script>
    

    You will need to modify parts of it because I changed some parameters around and don't reference your "app" object.

    For what I did to fix it (from what I can recall).

    1. The parameters have to be alphabetical. I placed them in an array and then sort them. I follow this up by a join with the ampersand.

    2. I modified the sha256 function to return the base64 of the RAW sha256. Before it was returning the hexbits in lowercase, which isn't correct.

    3. I was going to add a base64 before encoding, but the sha256 now handles all of the signing.

    4. The date format was incorrect. It was returning a epoch timestamp instead of a string timestamp. I hacked together a simple timestamp option.

      This code requires you to include the Base64 Library for CryptoJS also.