Search code examples
amazon-web-servicesapigoogle-apps-script

Google Apps Script: Getting Orders from Amazon Selling Partner API (Signing Requests)


I'm trying to create a request to Amazon Selling Partner API following this guide.

The first part: Creating an access has already been taken care of here.

The documentation of the API for Orders can be found here.

I'm trying to invoke the GET /orders/v0/orders operation.

Connecting to the API

The only mandatory parameter for this operation is the MarketplaceIds based on the documentation.

In order to get the orders we need to sign our request. Here is my code so far:

function GetOrders(){
  var access_token = AccessToken();

  //Time variables
  var currentDate = new Date();
  var isoDate = currentDate.toISOString();
  var yearMonthDay= Utilities.formatDate(currentDate, 'GTM-5', 'yyyyMMdd');

  //API variables
  var end_point = 'https://sellingpartnerapi-eu.amazon.com';

  //Credential variables
  var aws_region = "eu-west-1";
  var service = "execute-api";
  var termination_string = "aws4_request";

  //CanonicalRequest = httpRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload));
  //CanonicalRequest components:
  var httpRequestMethod = 'GET';
  var canonicalURI = '/orders/v0/orders';
  var canonicalQueryString = '?marketplaceId=A1PA6795UKMFR9';
  var canonicalheaders = 'host:' + canonicalURI + '\n' + 'x-amz-access-token:' + access_token + '\n' + 'x-amz-date:' + isoDate;
  var signedheaders = 'host;user-agent;x-amz-access-token;x-amz-date';
  var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, "");//NEW
  requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");//NEW

  //Building the canonical request
  var canonical_string = httpRequestMethod + '\n' + canonicalURI + '\n' + canonicalQueryString + '\n' + canonicalheaders + '\n' + signedheaders + '\n' + requestPayloadHashed;//UPDATED
  var canonical_signature = Utilities.computeHmacSha256Signature(canonical_string, ACCESS_KEY);
  var canonical_request = canonical_string + '\n' + canonical_signature;
  canonical_request = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, canonical_request);//NEW

  //CredentialScope = Date + AWS region + Service + Termination string;
  //StringToSign = Algorithm + \n + RequestDateTime + \n + CredentialScope + \n + HashedCanonicalRequest;
  var credential_scope = yearMonthDay + '/' + aws_region + '/' + service + '/' + termination_string;
  var string_to_sign = "AWS4-HMAC-SHA256" + '\n' + isoDate + '\n' + credential_scope + '\n' + canonical_request;

  var kSecret = ACCESS_KEY;
  var kDate = Utilities.computeHmacSha256Signature(yearMonthDay, "AWS4" + kSecret);
  var kRegion = Utilities.computeHmacSha256Signature(Utilities.newBlob(aws_region).getBytes(), kDate);
  var kService = Utilities.computeHmacSha256Signature(Utilities.newBlob(service).getBytes(), kRegion);
  var kSigning = Utilities.computeHmacSha256Signature(Utilities.newBlob(termination_string).getBytes(), kService);
  kSigning = kSigning.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");
  Logger.log('kSigning: ' + kSigning);

  var signature = Utilities.computeHmacSha256Signature(kSigning, string_to_sign);
  signature = signature.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");

  var options = {
    'method': 'GET',
    'payload': {
      'end_point': end_point,
      'path': canonicalURI,
      'query_string': canonicalQueryString
      //Path parameter not needed
    },
    'headers': {
      //'host': end_point,
      'x-amz-access-token': access_token,
      'x-amz-date': isoDate,
      'user-agent': 'GAS Script 1.0 (Javascript)',
      'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
    },
  }
  
  var getOrders = UrlFetchApp.fetch(end_point, options);
  Logger.log(getOrders);
}

PROBLEMS

When running the script I get the following error:

    Exception: Request failed for https://sellingpartnerapi-eu.amazon.com returned code 403. Truncated server response: {
{
  "errors": [
    {
      "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.

The Canonical String for this request should have been
'POST
/

host:sellingpartnerapi-eu.amazon.com
user-agent:Mozilla/5.0 (compatible; Google-Apps-Script; beanserver; +https://script.google.com; id: UAEmdDd-KyWEWcR137UzUzWb1fu3rUgNviHA)
x-amz-access-token:Atza|IwEBSomeAccessToken
x-amz-date:2021-03-10T02:44:01.727Z

host;user-agent;x-amz-access-token;x-amz-date
cf22942946358a7530d8b72df6333e859644aaebb08a1cd825a6af65a8561111'

The String-to-Sign should have been
'AWS4-HMAC-SHA256
20210310T024401Z
20210310/eu-west-1/execute-api/aws4_request
c4c1dcea7026765f52c5265296f9e1cb91b6618928debbc04a393bac89ce8493'
",
     "code": "InvalidSignature"
    }
  ]
}

QUESTIONS

I have a big doubt about what is "Payload"

For this part of the code:

var canonical_string = httpRequestMethod + '\n' + canonicalURI + '\n' + canonicalQueryString + '\n' + canonicalheaders + '\n' + signedheaders + '\n' + requestPayloadHashed;

We have to incorporate a hashed version of the payload request requestPayloadHashed.

It also mentions:

If the payload is empty, use an empty string as the input to the hash function.

For now I have just create that variable with a blank value

var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, "");//NEW
      requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");//NEW

But I'm not sure if I'm ommiting something important there.

UPDATE #1

After applying Tanaike recommendations I got the following message:

{
  "errors": [
    {
      "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.

The Canonical String for this request should have been
'GET
/orders/v0/orders
marketplaceId=A1PA6795UKMFR9
host:sellingpartnerapi-eu.amazon.com
user-agent:Mozilla/5.0 (compatible; Google-Apps-Script; beanserver; +https://script.google.com; id: UAEmdDd-KyWEWcR137UzUzWb1fu3rUgNviHA)
x-amz-access-token:Atza|IwEBISomeAccessToken
x-amz-date:2021-03-10T03:00:14.411Z

host;user-agent;x-amz-access-token;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'

The String-to-Sign should have been
'AWS4-HMAC-SHA256
20210310T030014Z
20210310/eu-west-1/execute-api/aws4_request
f1bbc99190ca5a9e9e068ad6a0b2ef6a7aed4a1232095ef8f3d77ad62d0e66ac'
",
     "code": "InvalidSignature"
    }
  ]
}

UPDATE #2 There is this website that help us do some testing with these connections:

https://mws.amazonservices.de/scratchpad/index.html

By using it I believe I have validated the Access Key ID and Secret Key, however, it is asking for a SellerId which is new to me and is also not mentioned in the API docs.

I'm wondering where it could go.

enter image description here

UPDATE #3

I implemented most of Tanaike recommendations and also tried to align what I was sending to the API to the error message I was getting:

This is the last version of the script:

function GetOrders(){
  var access_token = AccessToken();

  //Time variables
  var currentDate = new Date();
  var isoDate = currentDate.toISOString();
  var isoString = isoDate.replace(/-/g, "").replace(/:/g, "").replace(/(\.\d{3})/, "");
  var yearMonthDay= Utilities.formatDate(currentDate, 'GTM-5', 'yyyyMMdd');
  Logger.log('isoDate: ' + isoDate)
  //API variables
  var end_point = 'https://sellingpartnerapi-eu.amazon.com';

  //Credential variables
  var aws_region = "eu-west-1";
  var service = "execute-api";
  var termination_string = "aws4_request";

  //CanonicalRequest = httpRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload));
  //CanonicalRequest components:
  var httpRequestMethod = 'GET';
  var canonicalURI = '/orders/v0/orders';
  var canonicalQueryString = '?marketplaceId=A1PA6795UKMFR9';
  var canonicalheaders = 'host:' + "sellingpartnerapi-eu.amazon.com" + '\n' + 'x-amz-access-token:' + access_token + '\n' + 'x-amz-date:' + isoDate;
  var signedheaders = 'host;x-amz-access-token;x-amz-date';//;user-agent
  var requestPayloadHashed = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, "");//NEW
  requestPayloadHashed = requestPayloadHashed.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");//NEW

  //Building the canonical request
  var canonical_string = httpRequestMethod + '\n' + canonicalURI + '\n' + "marketplaceId=A1PA6795UKMFR9" + '\n' + canonicalheaders + '\n\n' + signedheaders + '\n' + requestPayloadHashed;//UPDATED
  Logger.log('canonical_string: ' + canonical_string)
  var canonical_signature = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, canonical_string);
  canonical_request = canonical_signature.map(function(e) {return ("0" + (e < 0 ? e + 256 : e).toString(16)).slice(-2)}).join("");
  Logger.log("canonical_request: " + canonical_request)

  //CredentialScope = Date + AWS region + Service + Termination string;
  //StringToSign = Algorithm + \n + RequestDateTime + \n + CredentialScope + \n + HashedCanonicalRequest;
  var credential_scope = yearMonthDay + '/' + aws_region + '/' + service + '/' + termination_string;
  var string_to_sign = "AWS4-HMAC-SHA256" + '\n' + isoString + '\n' + credential_scope + '\n' + canonical_request;
  Logger.log("string_to_sign: " + string_to_sign);
  var kSecret = ACCESS_KEY;
  var kDate = Utilities.computeHmacSha256Signature(yearMonthDay, "AWS4" + kSecret);
  var kRegion = Utilities.computeHmacSha256Signature(toBytes(aws_region), kDate);
  var kService = Utilities.computeHmacSha256Signature(toBytes(service), kRegion);
  var kSigning = Utilities.computeHmacSha256Signature(toBytes(termination_string), kService);
  Logger.log('kSigning: ' + kSigning);

  var signature = hex(Utilities.computeHmacSha256Signature(toBytes(string_to_sign), kSigning));
  Logger.log('signature: ' + signature)
  var options = {
    'method': 'GET',
    'headers': {
      //'host': end_point,
      'x-amz-access-token': access_token,
      'x-amz-date': isoDate,
      //'user-agent': 'Mozilla/5.0 (compatible; Google-Apps-Script; beanserver; +https://script.google.com; id: UAEmdDd-KyWEWcR137UzUzWb1fu3rUgNviHA)',
      'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
    },
    'muteHttpExceptions': true
  }
  
  var getOrders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
  Logger.log(getOrders);
}

I'm now getting an error related entirely to my access:

{
  "errors": [
    {
      "message": "Access to requested resource is denied.",
     "code": "Unauthorized",
     "details": ""
    }
  ]
}

However this is probably due to the fact that when I registered the application (guide here) I used the IAM user instead of the IAM role.

And it says in the guide that:

Important. When registering your application, the IAM ARN that you provide must be for the IAM entity to which you attached the IAM policy from Step 3. Create an IAM policy. In this workflow, that IAM entity is the IAM role from Step 4. Create an IAM role. If you register your application using your IAM user, be sure that the IAM policy is attached to it. Otherwise your calls to the Selling Partner API will fail. We recommend registering your application using an IAM role, as shown in this workflow, to help you better control access to your AWS resources.

So I'm going to go ahead and fix that and see if I get the authorization I need.


Solution

  • Modification points:

    • In the case of UrlFetchApp, when payload is used, even when method is GET, it is requested as the POST request. It seems that this is the current specification.
    • user-agent cannot be changed for UrlFetchApp.

    As a precondition, when your values for authorizing are correct values for requesting to the endpoint, your script can be modified by reflected above points as follows.

    I thought that your error message might be due to the difference between the method of "GET" and "POST". At first, please test the following modification. When an error occurs, please show it.

    Modified script:

    From:
    var options = {
      'method': 'GET',
      'payload': {
        'end_point': end_point,
        'path': canonicalURI,
        'query_string': canonicalQueryString
        //Path parameter not needed
      },
      'headers': {
        //'host': end_point,
        'x-amz-access-token': access_token,
        'x-amz-date': isoDate,
        'user-agent': 'GAS Script 1.0 (Javascript)',
        'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
      },
    }
    
    var getOrders = UrlFetchApp.fetch(end_point, options);
    
    To:
    var options = {
      'method': 'GET',
      'headers': {
        'x-amz-access-token': access_token,
        'x-amz-date': isoDate,
        'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
      },
    }
    
    var getOrders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
    

    Reference:

    Added:

    From Signing AWS requests with Signature Version 4, I modified your script. When I saw your script, I noticed that the byte array is included in the string values. I thought that this might be also one of reasons of your issue. So I modified your script. Could you please confirm it? And when I saw the official document, I confirmed that when the byte array is used for Utilities.computeHmacSha256Signature, converting the string value to byte array is the same with the samples rather than converting the byte array to string value.

    function sample() {
      const hex = bytes => bytes.map(byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
      const digestToHex = data => hex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, data));
      const toBytes = data => Utilities.newBlob(data).getBytes();
    
      const ACCESS_ID = "MyAccessKey";
      const ACCESS_KEY = "MyAccessSecret";
      var access_token = "access_token"; // AccessToken();
    
      //Time variables
      var currentDate = new Date();
      var isoDate = currentDate.toISOString();
      var yearMonthDay= Utilities.formatDate(currentDate, 'GTM-5', 'yyyyMMdd');
    
      //API variables
      var end_point = 'https://sellingpartnerapi-eu.amazon.com';
    
      //Credential variables
      var aws_region = "eu-west-1";
      var service = "execute-api";
      var termination_string = "aws4_request";
    
      // 1. Create string to sign.
      var httpRequestMethod = 'GET';
      var canonicalURI = '/orders/v0/orders';
      var canonicalQueryString = '?marketplaceId=A1PA6795UKMFR9';
      var canonicalheaders = 'host:' + canonicalURI + '\n' + 'x-amz-access-token:' + access_token + '\n' + 'x-amz-date:' + isoDate;
      var signedheaders = 'host;user-agent;x-amz-access-token;x-amz-date';
      const canonicalRequest = [httpRequestMethod,canonicalURI,canonicalQueryString,canonicalheaders + "\n",signedheaders,digestToHex("")].join("\n");
      const canonical_request = digestToHex(canonicalRequest);
      var credential_scope = yearMonthDay + '/' + aws_region + '/' + service + '/' + termination_string;
      var string_to_sign = "AWS4-HMAC-SHA256" + '\n' + isoDate + '\n' + credential_scope + '\n' + canonical_request;
    
      // 2. Create derived signing key.
      var kSecret = ACCESS_KEY;
      var kDate = Utilities.computeHmacSha256Signature(yearMonthDay, "AWS4" + kSecret);
      var kRegion = Utilities.computeHmacSha256Signature(toBytes(aws_region), kDate);
      var kService = Utilities.computeHmacSha256Signature(toBytes(service), kRegion);
      var kSigning = Utilities.computeHmacSha256Signature(toBytes(termination_string), kService);
    
      // 3. Create signature.
      const signature = hex(Utilities.computeHmacSha256Signature(toBytes(string_to_sign), kSigning));
    
      // 4. Request.
      var options = {
        'method': 'GET',
        'headers': {
          'x-amz-access-token': access_token,
          'x-amz-date': isoDate,
          'Authorization': 'AWS4-HMAC-SHA256 Credential=' + ACCESS_ID + '/' + credential_scope + ', SignedHeaders=' + signedheaders + ', Signature=' + signature,
        },
      }
      var getOrders = UrlFetchApp.fetch(end_point + canonicalURI + canonicalQueryString, options);
      Logger.log(getOrders);
    }
    

    Note:

    • Unfortunately, I cannot test above modified script. So when you tested it and an error occurs, please confirm your values for authorizating again. And please show the error message.

    • In this modification, it supposes that your values for authorizating are the correct values. Please be careful this.

    • About const canonicalRequest = [httpRequestMethod,canonicalURI,canonicalQueryString,canonicalheaders + "\n",signedheaders,digestToHex("")].join("\n");, when above script occurs an error, please test const canonicalRequest = [httpRequestMethod,canonicalURI,canonicalQueryString,canonicalheaders + "\n",""].join("\n");.

    Reference: