collumbo
collumbo

Reputation: 527

Amazon Alexa Web Service always gives 401

Problem solved -- see Update 2 at end of description. The below code is fine

Tearing my hair out here... but here it goes:

I am trying to hook into the Amazon Alexa API (http://docs.aws.amazon.com/AlexaWebInfoService/latest/index.html?ApiReference_UrlInfoAction.html) data from the API... I need to use C#.

I have updated this post below with Java Code I used to see if it's my code problem or AWIS problem.

Regarding the C#, the guy at the end of this post claims to have it working: https://forums.aws.amazon.com/message.jspa?messageID=476573#476573

This is the C# code that calls the class:

  var awis = new AmazonAWIS
                {
                    AWSAccessKeyId = "ABCDRFGHIJKLMNOP",
                    AWSSecret = "GpC0PcXnnzG/TCpoi9r7RxBtqCzdKaHeEkq7Mfs6"    
                };

  awis.UrlInfo("bbc.co.uk");

And this is the class code taken directly from the link posted above... I have not changed it:

public class AmazonAWIS
{
    public string AWSAccessKeyId { get; set; }
    public string AWSSecret { get; set; }

    protected string GenerateSignature(string param)
    {
        var sign = "GET\n" + "awis.amazonaws.com" + "\n/\n" + param;

        // create the hash object
        var shaiSignature = new HMACSHA256(Encoding.UTF8.GetBytes(AWSSecret));

        // calculate the hash
        var binSig = shaiSignature.ComputeHash(Encoding.UTF8.GetBytes(sign));

        // convert to hex
        var signature = Convert.ToBase64String(binSig);

        return signature;
    }

    // this is one of the key problems with the Amazon code and C#.. C# by default returns excaped values in lower case
    // for example %3a but Amazon expects them in upper case i.e. %3A, this function changes them to upper case..
    //
    public static string UpperCaseUrlEncode(string s)
    {
        char[] temp = HttpUtility.UrlEncode(s).ToCharArray();
        for (int i = 0; i < temp.Length - 2; i++)
        {
            if (temp[i] == '%')
            {
                temp[i + 1] = char.ToUpper(temp[i + 1]);
                temp[i + 2] = char.ToUpper(temp[i + 2]);
            }
        }
        return new string(temp);
    }

    string GetQueryParams(string action, Dictionary<string, string> extra)
    {
        var time = DateTime.UtcNow;

        // set the correct format for the date string
        var timestamp = time.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", System.Globalization.CultureInfo.InvariantCulture);

        // create a sortable dict
        var vals = new Dictionary<string, string>();

        vals.Add("AWSAccessKeyId", AWSAccessKeyId);
        vals.Add("Action", action);
        vals.Add("ResponseGroup", "Rank,ContactInfo,LinksInCount");
        vals.Add("Timestamp", timestamp);
        vals.Add("Count", "10");
        vals.Add("Start", "1");
        vals.Add("SignatureVersion", "2");
        vals.Add("SignatureMethod", "HmacSHA256");

        // add any extra values
        foreach (var v in extra)
        {
            if (vals.ContainsKey(v.Key) == false)
                vals.Add(v.Key, v.Value);
        }

        // sort the values by ordinal.. important!
        var sorted = vals.OrderBy(p => p.Key, StringComparer.Ordinal).ToArray();

        var url = new StringBuilder();

        foreach (var v in sorted)
        {
            if (url.Length > 0)
                url.Append("&");

            url.Append(v.Key + "=" + UpperCaseUrlEncode(v.Value));
        }

        return url.ToString();
    }

    public void UrlInfo(string domain)
    {
        string request = "UrlInfo";

        // add the extra values
        var extra = new Dictionary<string, string>();
        extra.Add("Url", domain);

        // run the request with amazon
        try
        {
            var res = RunRequest(request, extra);

            // process the results...
            Console.WriteLine(res);
        }
        catch (Exception ex)
        {
            throw;
        }
    }

    private string RunRequest(string request, Dictionary<string, string> extra)
    {
        // generate the query params
        var queryParams = GetQueryParams(request, extra);

        // calculate the signature
        var sig = GenerateSignature(queryParams);

        // generate the url
        var url = new StringBuilder();
        url.Append("http://awis.amazonaws.com?");
        url.Append(queryParams);
        url.Append("&Signature=" + UpperCaseUrlEncode(sig));

        // get the request

        var c = new WebClient();
        var res = c.DownloadString(url.ToString());
        return res;
    }
}

It fails on the line:

var res = c.DownloadString(url.ToString());

I always get a 401 Unauthorized...

Any idea what I am doing wrong?

Update

I can reproduce this same problem with their Java Sample Application. I have modified their app to just hard code the AWSAccessId and SecretKey, and I also don't use the sun.misc.BASE64Encoder which is in their app.

The exact code is below... again, If I grab the uri from the makeRequest(uri) statement, paste into Fiddler, I can see it's the same 401 response:

<?xml version="1.0"?>

AuthFailureAWS was not able to validate the provided access credentialsff8f1853-b816-47a0-2283-be9941e7f2a9

And the code that caused the above (I have changed the accessKey and secretKey):

package urlinfo.com;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.SignatureException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
* Makes a request to the Alexa Web Information Service UrlInfo action.
*/
public class UrlInfo {

private static final String ACTION_NAME = "UrlInfo";
private static final String RESPONSE_GROUP_NAME = "Rank,ContactInfo,LinksInCount";
private static final String SERVICE_HOST = "awis.amazonaws.com";
private static final String AWS_BASE_URL = "http://" + SERVICE_HOST + "/?";
private static final String HASH_ALGORITHM = "HmacSHA256";

private static final String DATEFORMAT_AWS = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

private String accessKeyId;
private String secretAccessKey;
private String site;

public UrlInfo(String accessKeyId, String secretAccessKey, String site) {
    this.accessKeyId = accessKeyId;
    this.secretAccessKey = secretAccessKey;
    this.site = site;
}

/**
 * Generates a timestamp for use with AWS request signing
 *
 * @param date current date
 * @return timestamp
 */
protected static String getTimestampFromLocalTime(Date date) {
    SimpleDateFormat format = new SimpleDateFormat(DATEFORMAT_AWS);
    format.setTimeZone(TimeZone.getTimeZone("GMT"));
    return format.format(date);
}

/**
 * Computes RFC 2104-compliant HMAC signature.
 *
 * @param data The data to be signed.
 * @return The base64-encoded RFC 2104-compliant HMAC signature.
 * @throws java.security.SignatureException
 *          when signature generation fails
 */
protected String generateSignature(String data)
        throws java.security.SignatureException {
    String result;
    try {
        // get a hash key from the raw key bytes
        SecretKeySpec signingKey = new SecretKeySpec(
                secretAccessKey.getBytes(), HASH_ALGORITHM);

        // get a hasher instance and initialize with the signing key
        Mac mac = Mac.getInstance(HASH_ALGORITHM);
        mac.init(signingKey);

        // compute the hmac on input data bytes
        byte[] rawHmac = mac.doFinal(data.getBytes());

        // base64-encode the hmac
        // result = Encoding.EncodeBase64(rawHmac);
        // result = new BASE64Encoder().encode(rawHmac);
        result = javax.xml.bind.DatatypeConverter.printBase64Binary(rawHmac);

    } catch (Exception e) {
        throw new SignatureException("Failed to generate HMAC : "
                + e.getMessage());
    }
    return result;
}

/**
 * Makes a request to the specified Url and return the results as a String
 *
 * @param requestUrl url to make request to
 * @return the XML document as a String
 * @throws IOException
 */
public static String makeRequest(String requestUrl) throws IOException {
    URL url = new URL(requestUrl);
    URLConnection conn = url.openConnection();
    InputStream in = conn.getInputStream();

    // Read the response
    StringBuffer sb = new StringBuffer();
    int c;
    int lastChar = 0;
    while ((c = in.read()) != -1) {
        if (c == '<' && (lastChar == '>'))
            sb.append('\n');
        sb.append((char) c);
        lastChar = c;
    }
    in.close();
    return sb.toString();
}


/**
 * Builds the query string
 */
protected String buildQuery()
        throws UnsupportedEncodingException {
    String timestamp = getTimestampFromLocalTime(Calendar.getInstance().getTime());

    Map<String, String> queryParams = new TreeMap<String, String>();
    queryParams.put("Action", ACTION_NAME);
    queryParams.put("ResponseGroup", RESPONSE_GROUP_NAME);
    queryParams.put("AWSAccessKeyId", accessKeyId);
    queryParams.put("Timestamp", timestamp);
    queryParams.put("Url", site);
    queryParams.put("SignatureVersion", "2");
    queryParams.put("SignatureMethod", HASH_ALGORITHM);

    String query = "";
    boolean first = true;
    for (String name : queryParams.keySet()) {
        if (first)
            first = false;
        else
            query += "&";

        query += name + "=" + URLEncoder.encode(queryParams.get(name), "UTF-8");
    }

    return query;
}

/**
 * Makes a request to the Alexa Web Information Service UrlInfo action
 */
public static void main(String[] args) throws Exception {

    String accessKey = "REMOVED";
    String secretKey = "REMOVED";
    // String site = args[2];
    String site = "www.google.com";

    UrlInfo urlInfo = new UrlInfo(accessKey, secretKey, site);

    String query = urlInfo.buildQuery();

    String toSign = "GET\n" + SERVICE_HOST + "\n/\n" + query;

    System.out.println("String to sign:\n" + toSign + "\n");

    String signature = urlInfo.generateSignature(toSign);

    String uri = AWS_BASE_URL + query + "&Signature=" +
            URLEncoder.encode(signature, "UTF-8");

    System.out.println("Making request to:\n");
    System.out.println(uri + "\n");

    // Make the Request

    String xmlResponse = makeRequest(uri);

    // Print out the XML Response

    System.out.println("Response:\n");
    System.out.println(xmlResponse);
  }
}

UPDATE 2:

No problem with this code. The problem was with the AWIS Sign up button. Use the one at the bottom of the page... not the one at the top (which is what most people would click). Amazon confirmed that the top button is not working currently.

Upvotes: 1

Views: 1834

Answers (1)

Umang
Umang

Reputation: 211

I received the same error via my php code even after the dysfunctional buttons bug was handled. See here

For me the error was because of the IAM credentials that I was using which are still not compatible with AWIS and this hasn't been documented here. One should use root account details for AWIS. See here

Upvotes: 2

Related Questions