mhart / aws4

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

AWS signature does not match #123

Closed ramsenconstantine closed 3 years ago

ramsenconstantine commented 3 years ago

With the following code I see this error:

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.

I grabbed the keys generated and tried them in postman and the request works so something must be off with the signature. Am I using this incorrectly?

exports.awsGETRequest = async (url, apiEndPoint, params) => {
    let role = await sts.getRole() 
    var opts = { host: url, path: apiEndPoint + params, service: prop.serviceName, region: prop.awsRegion }
    aws4.sign(opts, { accessKeyId: role.Credentials.AccessKeyId , secretAccessKey: role.Credentials.SecretAccessKey })
    try {
        let res = await supertest(url).get(apiEndPoint + params).retry(2)
            .set('X-Amz-Security-Token', role.Credentials.SessionToken)
            .set('X-Amz-Date', opts.headers["X-Amz-Date"])
            .set('Authorization', opts.headers.Authorization)
        return res;
    } catch (err) {
        console.log('Error in sending GET Request: ', err);
    }
};
mhart commented 3 years ago

No idea what supertest is I'm afraid – I suspect the problem is with how you're using that.

Try using plain Node.js requests like in the README and see if you can track down the issue.

mhart commented 3 years ago

On initial glance it looks like you're using a session token, but not signing the request with it. See the README for details on how to include it: https://github.com/mhart/aws4#aws4signrequestoptions-credentials

ramsenconstantine commented 3 years ago

I have tried with the session token in aws4.sign as well, but it doesn't make a difference. I can try another method of hitting the endpoint.

mhart commented 3 years ago

This looks suspicious: host: url

I don't know the value of url – but a URL is not a host

ramsenconstantine commented 3 years ago

One thing I should mention is the code worked in July and I just revisited it. In my case what is passed as url is something like n1sd4n2bl9.execute-api.us-west-1.amazonaws.com.

mhart commented 3 years ago

The code you posted shouldn't have been working, even in July – not without the session token in the signature

ramsenconstantine commented 3 years ago

It is odd, but I'm looking at my old jenkins logs and comparing them to my github check ins at the time. That code was working for whatever reason.

mhart commented 3 years ago

I guess if the session token was undefined, then it might've worked.

In any case, this appears to be a problem with a third-party library (supertest).

Unless there's some code that reproduces the error using standard Node.js requests, I'm afraid I can't help.

ramsenconstantine commented 3 years ago

I was able to reproduce the same issue using requests:

exports.awsGETRequest = async (url, apiEndPoint, params) => {
    let role = await sts.getRole()
    var opts = { host: url, path: apiEndPoint + params, service: prop.serviceName, region: prop.awsRegion }
    aws4.sign(opts, { accessKeyId: role.Credentials.AccessKeyId, secretAccessKey: role.Credentials.SecretAccessKey, sessionToken: role.Credentials.SessionToken })

    var options = {
        'method': 'GET',
        'url': url + apiEndPoint + params,
        'headers': {
            'X-Amz-Security-Token': role.Credentials.SessionToken,
            'X-Amz-Date': opts.headers["X-Amz-Date"],
            'Authorization': opts.headers.Authorization
        }
    };
    request(options, function (error, response) {
        if (error) throw new Error(error);
        console.log(response.body);
        return response
    });
};
mhart commented 3 years ago

I think you're using the 3rd party request module – not the standard Node.js https module.

Please see the README for usage:

https://github.com/mhart/aws4#example

Try something like this for debugging purposes:

const https = require('https')

exports.awsGETRequest = async (url, apiEndPoint, params) => {
  let role = await sts.getRole()

  var opts = { host: url, path: apiEndPoint + params, service: prop.serviceName, region: prop.awsRegion }

  aws4.sign(opts, { accessKeyId: role.Credentials.AccessKeyId, secretAccessKey: role.Credentials.SecretAccessKey, sessionToken: role.Credentials.SessionToken })

  https.request(opts, function(res) { res.pipe(process.stdout) }).end(opts.body || '')
}
ramsenconstantine commented 3 years ago

Same result with https. I'm starting to think something on the backend may be causing this since it used to work.

exports.awsGETRequest = async (host, apiEndPoint, params) => {
    let role = await sts.getRole()
    var opts = { host: host, path: apiEndPoint + params, service: prop.serviceName, region: prop.awsRegion }
    aws4.sign(opts, { accessKeyId: role.Credentials.AccessKeyId, secretAccessKey: role.Credentials.SecretAccessKey, sessionToken: role.Credentials.SessionToken })

    var options = {
        'method': 'GET',
        'host': host,
        'path': apiEndPoint + params,
        'headers': {
            'X-Amz-Security-Token': role.Credentials.SessionToken,
            'X-Amz-Date': opts.headers["X-Amz-Date"],
            'Authorization': opts.headers.Authorization
        }
    };
    var req = https.request(options, (res) => {
        res.on('data', (d) => {
            process.stdout.write(d);
        });
    });

    req.on('error', error => {
        console.error(error)
    })

    req.end()
};
mhart commented 3 years ago

I'm not sure why you keep redeclaring the options – aws4 signs standard Node.js options. Just do this:

exports.awsGETRequest = async (host, apiEndPoint, params) => {
    let role = await sts.getRole()
    var opts = { host: host, path: apiEndPoint + params, service: prop.serviceName, region: prop.awsRegion }
    aws4.sign(opts, { accessKeyId: role.Credentials.AccessKeyId, secretAccessKey: role.Credentials.SecretAccessKey, sessionToken: role.Credentials.SessionToken })

    var req = https.request(opts, (res) => {
        res.on('data', (d) => {
            process.stdout.write(d);
        });
    });

    req.on('error', error => {
        console.error(error)
    })

    req.end()
};

Maybe you need to print out some debugging info.

Like apiEndPoint and params – do they make sense to join together as a path with no separator?

That would mean that apiEndPoint begins with a / and params is a string that begins with a ? correct?

ramsenconstantine commented 3 years ago

I have debugged that and the url is structured correctly. I do get this back in the response header though.

statusCode: 403
headers: {
  date: 'Mon, 28 Dec 2020 01:17:23 GMT',
  'content-type': 'application/json',
  'content-length': '1017',
  connection: 'close',
  'x-amzn-requestid': <guid>,
  'x-amzn-errortype': 'InvalidSignatureException',
  'x-amz-apigw-id': <id>
}

The reason I have opts and options is because I need opts.headers.Authorization in options, which has my header and GET as method.

mhart commented 3 years ago

Those options will already be set in opts – as the README says, this library signs Node.js options – so you don't need to modify or redeclare anything, you can you them directly with https.request

Unless there's something reproducible here I'm going to need to close this

BrianFanning commented 3 years ago

I ran into the same issue (request signature we calculated does not match the signature you provided), and it was also when I was trying to sign a request to invoke a method on an API Gateway, so I suspect we had the same problem:

I had the host set as the API Gateway endpoint with the stage name at the end: abcde1234.execute-api.ca-central-1.amazonaws.com/dev

however it must be using the endpoint name only, i.e. abcde1234.execute-api.ca-central-1.amazonaws.com

Then in the path you prepend the stage name to the method you are trying to access, like /dev/methodname

Hope this fixes it for you!

leoliang92 commented 3 years ago

Did you find a solution on this? I'm having the same issue. I can use the keys generated in my code on Postman without any issue. Am I missing anything??

Here is my code:

let options = {
        host: 'sellingpartnerapi-na.amazon.com',
        method: 'GET',
        path: '/vendors/orders/v1/purchaseOrders?createdAfter=2021-01-12',
        region: 'us-east-1',
        service: 'execute-api',
        headers: {
            'x-amz-access-token': fetchResponse.access_token
        }
    }
 let signedRequest = aws4.sign(options, {
        accessKeyId: role.Credentials.AccessKeyId, 
        secretAccessKey: role.Credentials.SecretAccessKey, 
        sessionToken: role.Credentials.SessionToken
    })

signedRequest.url = 'https://sellingpartnerapi-na.amazon.com/vendor/orders/v1/purchaseOrders?createdAfter=2021-01-12'

request(signedRequest, function(err, res, body) {
    console.dir(res.body);
});

I have tried using request, node-fetch, axios, and all of them having the same error.

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.

Oddly enough, when I used https, it returned a different error:

{
  "errors": [
    {
      "message": "Access to requested resource is denied.",
     "code": "Unauthorized",
     "details": ""
    }
  ]
}
mhart commented 3 years ago

@leoliang92 please see other threads about getting this working with sellingpartnerapi:

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

mhart commented 3 years ago

Closing this out