Request Signing, Amazon Product Advertising API, .NET C#




The Amazon Product Advertising API documentation provides some code samples for its use but none using ASP.NET. A personal interest brought me to play with it and as it wasn’t entirely trivial to create a signed request as required associate authentication I thought I’d share some working code samples.

Amazon Product Advertising API

Some notes

The API does surface a WSDL file and as such a Web Reference could be used to generate classes to interact with the API. The sample I am providing here does not take advantage of this and is instead submitting raw REST requests.

I see the most valuable part of this sample as the request signing piece. This sample should not be seen as a best practice for interacting with the API but rather as a utility for request signing.

The order of the query string parameters that are included in the signed string is crucial. They must be ordered by character code (in practise this equates to alphabetically, but with all upper case letters coming before any lower case letters). The API documentation suggests string splitting, sorting, and string joining. This is definitely the approach I would take if you find yourself writing queries that use parameters dynamically but I struggle to see the use-case. This sample just uses a hard-coded string with the relevant parameters in the correct order.

Although I haven’t looked in detail yet, the approach taken to sign requests here appears very similar if not identical to that required by the Instagram API, and I am sure many other (social media) APIs.

Requests to APIs which require the signing of a secret key cannot be made securely directly from the client (e.g. using JavaScript) as it would require your secret key to be available in plain text on the client. If you want to run ajax commands against the API you need execute requests to an intermediary service. This is the approach that the sample code below facilitates.

You can read about the Amazon Product Advertising API here: Product Advertising API

The code

Below you will find a class called the AmazonApiHelper. Further below is an ashx HttpHandler as an example of calling the utility functions provided by the helper class. You’ll need to provide you own values for the following constants:
private const string awsSecretKey = "Your secret key goes here";
private const string awsAccessKeyId = "Your access key Id goes here";
private const string associateTag = "Your associate tag goes here";

The helper class

using System;
using System.Security.Cryptography;
using System.Linq;
using System.Text;
using System.Web;
using System.IO;
using System.Net;
using System.Web.Script.Serialization;
using System.Xml.Linq;

namespace AmazonApp.Utilities
{
    public class AmazonApiHelper : IDisposable
    {
        #region Constants
        private const string EndPoint = "webservices.amazon.com";
        private const string RequestUri = "/onca/xml";

        private const string qsService = "AWSECommerceService";
        private const string qsOperation = "ItemSearch";
        private const string qsSearchIndex = "All";
        private const string qsResponseGroup = "Images,ItemAttributes,Offers";

        private const string canonicalQsFormat = "AWSAccessKeyId={2}&AssociateTag={3}&Keywords={5}&Operation={1}&ResponseGroup={6}&SearchIndex={4}&Service={0}&Timestamp={7}";

        private const string dateFormat = "yyyy-MM-ddTHH:mm:ss.000Z";
        private const string stringToSignFormat = "GET\n{0}\n{1}\n{2}";
        private const string signedUriFormat = "http://{0}{1}?{2}&Signature={3}";
        #endregion

        private HMACSHA256 hmac = null;

        public string AwsSecretKey { get; private set; }
        public string AwsAccessKeyId { get; private set; }
        public string AssociateTag { get; private set; }

        public AmazonApiHelper(string associateTag, string awsAccessKeyId, string awsSecretKey)
        {
            AssociateTag = associateTag;
            AwsAccessKeyId = awsAccessKeyId;
            AwsSecretKey = awsSecretKey;
        }

        public string GetRequestUri(string keywords)
        {
            string qsKeywords = HttpUtility.UrlPathEncode(keywords);
            string qsTimestamp = DateTime.UtcNow.ToString(dateFormat).Replace(":", "%3A");

            string canonicalQs = string.Format(canonicalQsFormat,
                                    qsService,
                                    qsOperation,
                                    AwsAccessKeyId,
                                    AssociateTag,
                                    qsSearchIndex,
                                    qsKeywords,
                                    qsResponseGroup.Replace(",", "%2C"),
                                    qsTimestamp);

            string stringToSign = string.Format(stringToSignFormat, EndPoint, RequestUri, canonicalQs);

            byte[] hashedSecretString = hmacSHA256(stringToSign, AwsSecretKey);
            string qsSignature = Convert.ToBase64String(hashedSecretString).Replace("+", "%2B").Replace("=", "%3D");

            string signedUri = string.Format(signedUriFormat, EndPoint, RequestUri, canonicalQs, qsSignature);
            return signedUri;
        }

        public string ExecuteWebRequest(string uri, string keywords)
        {
            string responseJson = null;

            WebRequest webRequest = WebRequest.CreateHttp(uri);
            using (HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse)
            using (Stream dataStream = webResponse.GetResponseStream())
            using (StreamReader reader = new StreamReader(dataStream))
            {
                string responseFromServer = reader.ReadToEnd();
                responseJson = GetWebResponseJson(uri, keywords, responseFromServer, webResponse);
            }

            return responseJson;
        }

        #region Utilities
        private string GetXmlValue(XElement el, params string[] elNames)
        {
            if(el == null)
            {
                return null;
            }
            if(elNames == null || elNames.Length < 1)
            {
                return el.Value;
            }
            XElement currentNode = el;
            foreach (string elName in elNames)
            {
                currentNode = currentNode.Elements().FirstOrDefault(e => e.Name.LocalName == elName);
                if(currentNode == null)
                {
                    // Path is not valid
                    return null;
                }
            }
            string valueOfFinalEl = currentNode.Value;
            return valueOfFinalEl;
        }

        private string GetWebResponseJson(string requestUri, string keywords, string responseFromServer, HttpWebResponse webResponse)
        {
            var xmlDoc = XDocument.Parse(responseFromServer);
            var itemsEl = xmlDoc.Descendants().First(d => d.Name.LocalName == "Items");
            var itemEls = itemsEl.Elements().Where(e => e.Name.LocalName == "Item");

            var items = itemEls.Select(i => new
            {
                asin = GetXmlValue(i, "ASIN"),
                productUrl = GetXmlValue(i, "DetailPageURL"),
                productImgUrl = GetXmlValue(i, "MediumImage", "URL") ?? GetXmlValue(i.Descendants().FirstOrDefault(e => e.Name.LocalName == "MediumImage"), "URL"),
                title = GetXmlValue(i, "ItemAttributes", "Title"),
                price = GetXmlValue(i, "OfferSummary", "LowestNewPrice", "FormattedPrice"),
                offersUrl = GetXmlValue(i, "Offers", "MoreOffersUrl")
            });

            JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
            var json = jsonSerializer.Serialize(new
            {
                keywords = keywords,
               // requestUri = requestUri,
                responseArray = items,
                statusDescription = webResponse.StatusDescription,
                statusCode = webResponse.StatusCode.ToString(),
                isError = webResponse.StatusCode != HttpStatusCode.OK,
                isFromCache = webResponse.IsFromCache
            });
            return json;
        }

        public static string GetEmptyResponseJson(string keywords, string message, HttpStatusCode code)
        {

            JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
            var json = jsonSerializer.Serialize(new
            {
                keywords = keywords,
               // requestUri = requestUri,
                responseArray = new object[0],
                statusDescription = message,
                statusCode = code.ToString(),
                isError = code != HttpStatusCode.OK,
                isFromCache = false
            });
            return json;
        }

        private byte[] hmacSHA256(string data, string key)
        {
            if (hmac == null) {
                hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
            }
            return hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
        }
        #endregion

        #region IDisposable Support
        private bool disposedValue = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                hmac.Dispose();
                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }
        #endregion
    }
}

 

Calling the helper class from a web handler

<%@ WebHandler Language="C#" Class="Handler" %>

using System;
using System.Net;
using System.Web;
using RankazonSPA.Utilities;

public class Handler : IHttpHandler
{
    private const string awsSecretKey = "xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxX";
    private const string awsAccessKeyId = "xXxXxXxXxXxXxXxXxXxX";
    private const string associateTag = "xXxXxXxXxXxXxX-21";

    public void ProcessRequest(HttpContext context)
    {
        var keywords = context.Request.QueryString["k"];
        string responseJson = string.Empty;

        if (string.IsNullOrEmpty(keywords))
        {
            responseJson = AmazonApiHelper.GetEmptyResponseJson(string.Empty, string.Empty, HttpStatusCode.OK);
        }
        else {
            using (AmazonApiHelper apiHelper = new AmazonApiHelper(associateTag, awsAccessKeyId, awsSecretKey))
            {
                string requestUri = apiHelper.GetRequestUri(keywords);
                try
                {
                    responseJson = apiHelper.ExecuteWebRequest(requestUri, keywords);
                }
                catch (Exception ex)
                {
                    responseJson = AmazonApiHelper.GetEmptyResponseJson(keywords, ex.Message, HttpStatusCode.InternalServerError);
                }
            }
        }

        context.Response.ContentType = "application/json";
        context.Response.Write(responseJson);
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

Good luck advertising those products!

Paul.




One thought on “Request Signing, Amazon Product Advertising API, .NET C#”

Leave a Reply

Your email address will not be published.