mhart / aws4

Signs and prepares Node.js requests using AWS Signature Version 4
MIT License
703 stars 176 forks source link

Access to requested resource is denied #125

Closed leoliang92 closed 3 years ago

leoliang92 commented 3 years ago

Hi guys, we also posted this question under Amazon selling partner api doc repository, but did not get a solution. Here is the link: https://github.com/amzn/selling-partner-api-models/issues/998 It seems out code is fine, but not sure if we did something wrong when we tried to sign the request. We decided to ask it agin here. Hopefully someone can help us out on this issue. Thank you!!

We have decided to implement our own solution to call SP-API. After going through all the documentation and cases we could find for the past five days, we still cannot get a successful response back. We have set up everything it should be and tested with Postman without any issue, but we just simply cannot get it working in our code. Here is the error we got:

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

Code we wrote:

const fetch = require('node-fetch')
const aws4 = require('aws4')
const AWS = require('aws-sdk');
const sts = new AWS.STS({apiVersion: '2011-06-15', accessKeyId: 'AKIAZ......', secretAccessKey: '1vNyD1ggSw....', region: 'us-east-1'})
const https = require('https')

module.exports = async (req, res) => {

    // build fetch options for fetching access token 
    let fetchOptions = {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: new URLSearchParams({
            'grant_type': 'refresh_token',
            'refresh_token': 'Atzr|IwGDFGSDFGr.....',
            'client_id': 'amzn1.application-oa2-client.80....',
            'client_secret': 'c98554551....'
        })
      };

    // get access token
    let fetchResponse = await fetch('https://api.amazon.com/auth/o2/token', fetchOptions).then(res => res.json())

    // call STS assume role
    let role = await sts.assumeRole({
        RoleArn: 'arn:aws:iam::631022162040:role/SellingPartnerAPI',
        RoleSessionName: 'sp-api'
    }).promise()

    // sign the request using token and credentials from previous steps
    let signedRequest = aws4.sign({
        host: 'sellingpartnerapi-na.amazon.com',
        path: '/vendors/orders/v1/purchaseOrders?createdAfter=2021-01-12',
        method: 'GET',
        service: 'execute-api',
        region: 'us-east-1',
        headers: {
            'user-agent': 'mlabs/0.1 (Language=JavaScript; Platform=Node)',
            'x-amz-access-token': fetchResponse.access_token
        }
    }, {
        accessKeyId: role.Credentials.AccessKeyId, 
        secretAccessKey: role.Credentials.SecretAccessKey, 
        sessionToken: role.Credentials.SessionToken
    })

    let response = await getAPIResponse(signedRequest)

    res.end(response)
}

// make API call using https
async function getAPIResponse(signedRequest) {
    return new Promise((resolve, reject) => {
        https.request(signedRequest, (res) => {
          let data = '';
          const {statusCode} = res;

          res.on('data', chunk => (data += chunk));
          res.on('end', async() => {
            const res = data && JSON.parse(data);

            if (statusCode === 200) {
              resolve(res);
            } else {
                console.log(res.errors)
              reject({statusCode, res});
            }
          });
        }).end();
      });
}

Here is the sample response from role request:

{
  ResponseMetadata: { RequestId: 'a884066.....' },
  Credentials: {
    AccessKeyId: 'ASIAZF2.....',
    SecretAccessKey: 'Pey/nLVCU.......',
    SessionToken: 'FwoGZXIvYXdzEKf//////////wEaDKa.....',
    Expiration: 2021-01-29T22:48:26.000Z
  },
  AssumedRoleUser: {
    AssumedRoleId: 'AROAZ....i',
    Arn: 'arn:aws:sts::631.....'
  }
}

Sample response from signedRequest:

{
  host: 'sellingpartnerapi-na.amazon.com',
  path: '/vendors/orders/v1/purchaseOrders?createdAfter=2021-01-12',
  method: 'GET',
  service: 'execute-api',
  region: 'us-east-1',
  headers: {
    'user-agent': 'mlabs/0.1 (Language=JavaScript; Platform=Node)',
    'x-amz-access-token': 'Atza|IwEBIPHh......',
    Host: 'sellingpartnerapi-na.amazon.com',
    'X-Amz-Security-Token': 'FwoXZYIxYydzEOj//////////wEa....',
    'X-Amz-Date': '20210201T142903Z',
    Authorization: 'AWS4-HMAC-SHA256 Credential=ASIAZF262CB4C2WIRG7N/20210201/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date;x-amz-security-token, Signature=b14da11bd30a5a96a.....'
  }
}

If we pasted the accessToken, accessKeyId, secretAccessKey, and sessionToken into Postman, we get a successful response back. Any help will be appreciated! We really need help to get this working....Thank you!!

mhart commented 3 years ago

Duplicate of https://github.com/mhart/aws4/issues/113#issuecomment-639291510 and https://github.com/mhart/aws4/issues/121#issuecomment-730849155

Access to requested resource is denied means that the code was signed correctly, but there are issues with your authentication – so aws4 is working correctly here.

It's most likely an issue with the way you're using the temporary credentials – try using non-temporary credentials and see if that works

mhart commented 3 years ago

Will Postman show you the headers and path it's sending? If so, see if you can determine what's different

mhart commented 3 years ago

Also make sure you've setup your IAM role correctly: https://github.com/amzn/selling-partner-api-docs/commit/d5c43fd770795b66bb56cb5edbf501392fe4ca3d#diff-0688a50a7c1991ab58a48a0333ced82e2a1be0073533a73ca1e89546bfb67f42

leoliang92 commented 3 years ago

Hi @mhart,

Thank you so much for your response. I have looked into amzn/selling-partner-api-models#771 & amzn/selling-partner-api-models#779 for at least 10 times, but everything we did seems matching the solutions provided there. We also double checked our IAM role is setup correctly. Otherwise, I don't think we can even get a response back from the postman.

You mentioned about using non-temporary credentials, and I though you have to use the temporary AccessKeyId, SecretAccessKey and SessionToken returned from the sts assume role method to sign the request for selling partner api? Could you give an example how to get non-temporary credentials and pass them into the request?

mhart commented 3 years ago

This example uses non-temporary credentials: https://github.com/mhart/aws4/issues/113#issuecomment-639291510

mhart commented 3 years ago

If you can show the difference between the headers (and anything else) that Postman is sending, and what you are sending here, then you might be able to see what needs to be changed.

leoliang92 commented 3 years ago

To be honest, I don't think amzn/selling-partner-api-models#771 should work at all. If we directly use non-temporary credentials, it will return the unauthorized error. His solution was more likely based on the old documentation. If you look at the code example here: https://vdanyliv.medium.com/amazon-selling-partner-api-spapi-how-to-quickly-and-simply-integrate-with-new-api-part-2-59d7458f24fe and here: https://github.com/amzn/selling-partner-api-models/issues/690

To get it work on postman, we need to have 3 steps.

  1. Get Access Token
  2. Get AccessKeyId, SecretAccessKey, SessionToken
  3. Use all four variables from last 2 steps to make the call

Here is a sample request headers from Postman

x-amz-access-token: Atza|IwEBIM8IZFu3v....
Host: sellingpartnerapi-na.amazon.com
X-Amz-Security-Token: FwoGZXIvYXdzEOn//////////wEaD....
X-Amz-Date: 20210201T151715Z
Authorization: AWS4-HMAC-SHA256 Credential=ASIAZF262CB4K3FXXEH4/20210201/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date;x-amz-security-token, Signature=f99824d135d76d8bd1bab505abdb....
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Postman-Token: c9a8d24a-015e-4db1-8a0....
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Here is the entire signed request we send through node https

{
  host: 'sellingpartnerapi-na.amazon.com',
  path: '/vendors/orders/v1/purchaseOrders?createdAfter=2021-01-12',
  method: 'GET',
  service: 'execute-api',
  region: 'us-east-1',
  headers: {
    'user-agent': 'mlabs/0.1 (Language=JavaScript; Platform=Node)',
    'x-amz-access-token': 'Atza|IwEBINZYKRi9b...',
    Host: 'sellingpartnerapi-na.amazon.com',
    'X-Amz-Security-Token': 'FwoGZXIvYXdzEOn//////////wEaDM5Y37fi....',
    'X-Amz-Date': '20210201T151726Z',
    Authorization: 'AWS4-HMAC-SHA256 Credential=ASIAZF262CB4JUS7YNF5/20210201/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date;x-amz-security-token, Signature=d3502c1d7347e8670aded8c978....'
  }
}
mhart commented 3 years ago

The access key id is different in those two requests – you sure you're using the same credentials?

mhart commented 3 years ago

Also the x-amz-access-token is different

mhart commented 3 years ago

Try to make all the credentials (accessToken, accessKeyId, secretAccessKey, and sessionToken) all exactly the same between the two methods: Postman and Node.js/aws4

Might be easier just to hardcode the credentials in your JS code to get this to work, ie:

    let signedRequest = aws4.sign({
        host: 'sellingpartnerapi-na.amazon.com',
        path: '/vendors/orders/v1/purchaseOrders?createdAfter=2021-01-12',
        method: 'GET',
        service: 'execute-api',
        region: 'us-east-1',
        headers: {
            'user-agent': 'mlabs/0.1 (Language=JavaScript; Platform=Node)',
            'x-amz-access-token': "MY_ACCESS_TOKEN"
        }
    }, {
        accessKeyId: "MY_ACCESS_KEY_ID", 
        secretAccessKey: "MY_SECRET_ACCESS_KEY", 
        sessionToken: "MY_SESSION_TOKEN"
    })
leoliang92 commented 3 years ago

@mhart Ok, I've changed them to use the same credentials. Same result. Here are the request headers:

Postman:

x-amz-access-token: Atza|IwEBILJjKIJLOacvUHiBWmVDDPaONR-WAmJdCiphXhnh-5fLi-SRq6Yj8VymFwffXK1QAIGOkQQJftZ0xjUJN1kaOXPTo....
Host: sellingpartnerapi-na.amazon.com
X-Amz-Security-Token: FwoGZXIvYXdzEOn//////////wEaDISg+6c02wF/DG9a8SKqATHqxQtYcoyw2+AE4hhMA5FSp9Tr2vQPX/NXo3QIy1pbKe5QLZRAv846zgWsf4f+nW3KrUyW0vIlq4qyJKZLPGvU7U/wPsBOJbOJKKGP4tbgru7EAbNb2i0XPKuFriJibahJ2URylVshymHZDpELnxDRYBK8fMxMaHl+Ll3ZzcMiHisG4nWY62eJbm/eE...
X-Amz-Date: 20210201T154924Z
Authorization: AWS4-HMAC-SHA256 Credential=ASIAZF262CB4KXO4YYOB/20210201/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date;x-amz-security-token, Signature=7a79f1742f9bf30af8392edac2327d524050b2355c68...
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Postman-Token: 687401b6-9cd1-47e3-be73-029d8cc46f05
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Node aws4:

{
  host: 'sellingpartnerapi-na.amazon.com',
  path: '/vendors/orders/v1/purchaseOrders?createdAfter=2021-01-12',
  method: 'GET',
  service: 'execute-api',
  region: 'us-east-1',
  headers: {
    'user-agent': 'mlabs/0.1 (Language=JavaScript; Platform=Node)',
    'x-amz-access-token': 'Atza|IwEBILJjKIJLOacvUHiBWmVDDPaONR-WAmJdCiphXhnh-5fLi-SRq6Yj8VymFwffXK1QAIGOkQQJftZ0xjUJN1kaOXPTomwI7GGnc9ntpYkThn-XL-WdSXhnAwnXzV10U7hO3HT-reVNjgk7QODAtUqo3VV_U19_H....',
    Host: 'sellingpartnerapi-na.amazon.com',
    'X-Amz-Security-Token': 'FwoGZXIvYXdzEOn//////////wEaDISg+6c02wF/DG9a8SKqATHqxQtYcoyw2+AE4hhMA5FSp9Tr2vQPX/NXo3QIy1pbKe5QLZRAv846zgWsf4f+nW3KrUyW0vIlq4qyJKZLPGvU7U/wPsBOJbOJKKGP4tbgru7EAbNb2i0XPKuFriJibahJ2URylVshymHZDpELnxDRYBK8fMxMaHl+Ll3ZzcMiHisG4nWY62e...',
    'X-Amz-Date': '20210201T155358Z',
    Authorization: 'AWS4-HMAC-SHA256 Credential=ASIAZF262CB4KXO4YYOB/20210201/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date;x-amz-security-token, Signature=7e4091417fc8e3c5ad4bf9041559bded0998f56....'
  }
}

Michael, you are our only hope now!!!! :( :( :(

mhart commented 3 years ago

Well everything looks ok from a request point of view – the Node.js request has everything the Postman one does, except for a couple of headers. You could try adding the Accept header and see if that makes a difference.

        headers: {
            'accept': '*/*',
            'user-agent': 'mlabs/0.1 (Language=JavaScript; Platform=Node)',
            'x-amz-access-token': "MY_ACCESS_TOKEN"
        }

Could also see if Connection: keep-alive is an issue too.

Can't really think of what else it could be. Again, this library is signing the request correctly (otherwise you'd be getting a signature error), so it could only be something else in the way the request is being made – a connection issue (you're not using a proxy?) or an https issue

leoliang92 commented 3 years ago

Ok, I've changed the the headers to include accept and connection, but same result.

Since you mentioned the request result. If I use other library to send the request, such as node-fetch, request, or axios, I will actually get a signature error as follows:

{
  "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
/vendor/orders/v1/purchaseOrders
createdAfter=2020-01-12
host:sellingpartnerapi-na.amazon.com
x-amz-access-token:Atza|IwEBIDvJFVhcwXRBco_cUjGP6X9VucwBpioq1FvHqI9X9LGVIQ9OoLaSzsVj7RsP0aXeAmd7WVmbNcRXolSSmGLbEz2_lpDvCdBeiBsmUOUV2QlEpAH5WITqitgsGRGQ90doKrWFNpF1tyduExzrilfI6WTfunShtzD-q9BVpVJLBUtquR81UhnFqHXLrm.....
x-amz-date:20210201T161938Z
x-amz-security-token:FwoGZXIvYXdzEOr//////////wEaDMDWahvQputexrSP0CKqAUgaw2tKwL5TKSQzc1rZtGClMHh4QACvzwftDi2stJsBBzHSjUsoaHlZPCX4O6n3d1pWmCkl8n1IEBY0Bq7Qv4lBVSwtQm1....

host;x-amz-access-token;x-amz-date;x-amz-security-token
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b....'

The String-to-Sign should have been
'AWS4-HMAC-SHA256
20210201T161938Z
20210201/us-east-1/execute-api/aws4_request
330f94161763d01e601b461db7ffc1b04a7e1e51c18babc553....'
",
     "code": "InvalidSignature"
    }
  ]
}

The request headers from node aws4

{
  host: 'sellingpartnerapi-na.amazon.com',
  path: '/vendors/orders/v1/purchaseOrders?createdAfter=2021-01-12',
  method: 'GET',
  service: 'execute-api',
  region: 'us-east-1',
  headers: {
    'user-agent': 'mlabs/0.1 (Language=JavaScript; Platform=Node)',
    'x-amz-access-token': 'Atza|IwEBIDvJFVhcwXRBco_cUjGP6X9VucwBpioq1FvHqI9X9LGVIQ9OoLaSzsVj7RsP0aXeAmd7WVmbNcRXolSSmGLbEz2_lpDvCdBeiBsmUOUV2QlEpAH5WITqitgsGRGQ90doKrWFNpF1tyduExzrilfI6WTfunShtzD-q9BVpVJLBUtquR81UhnFqHXLrmaXeclVfemJ7dii0Yu...',
    Host: 'sellingpartnerapi-na.amazon.com',
    'X-Amz-Security-Token': 'FwoGZXIvYXdzEOr//////////wEaDMDWahvQputexrSP0CKqAUgaw2tKwL5TKSQzc1rZtGClMHh4QACvzwftDi2stJsBBzHSjUsoaHlZPCX4O6n3d1pWmCkl8n1IEBY0Bq7Qv4lBVSwtQm14vVBBjhZ2rffcJRF...',
    'X-Amz-Date': '20210201T161938Z',
    Authorization: 'AWS4-HMAC-SHA256 Credential=ASIAZF262CB4HTMS6VXT/20210201/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-access-token;x-amz-date;x-amz-security-token, Signature=17756f970413ecfac3490a326c429953ad880c5eabf....'
  }
}

This is the code to make the fetch request:

const fetch = require('node-fetch')
let fetchResponse = await fetch('https://sellingpartnerapi-na.amazon.com/vendor/orders/v1/purchaseOrders?createdAfter=2020-01-12', 
      {
          method: 'GET',
          headers: signedRequest.headers
      }).then(res => res.text())