Receive 'SignatureDoesNotMatch' response when S3.getObject().promise() - amazon-web-services

First of all, I solved issue but I want to know exactly why this happened.
I'm using aws-sdk for javascript v2 like
await s3
.getObject({ Bucket: bucketName, Key: key }, (err, data) => {
if (err) {
throw new Error(`Can't get object from "${bucketName}/${key}".`);
}
})
.promise()
.then((data) => {
// decode data.Body
})
This code caused twice requests. (Sorry, I couldn't show you in detail.)
/GET /{some-url}
/GET /{some-url}
The difference between two requests is request header x-amz-user-agent.
One is aws-sdk-js/2.1248.0 callback.
The other one is aws-sdk-js/2.1248.0 callback promise.
And one which signature matches is aws-sdk-js/2.1248.0 callback.
The error response of other one is below.
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided.Check your key and signing method.</Message>
<AWSAccessKeyId>{ACCESSKEYIDEXAMPLE}</AWSAccessKeyId>
<StringToSign>
AWS4-HMAC-SHA256
20221123T001426Z
20221123/{region-example}/s3/aws4_request
29e50dd42fb05defb63ff9cd6d0ed6fd72d8078ac161d9a54fa704dbbeb88398
</StringToSign>
<SignatureProvided>4b302dcb0858f9c3d1289c9b957d86cd965f0b9909dbe88e08b68a4476f67e9c</SignatureProvided>
<StringToSignBytes>...</StringToSignBytes>
<CanonicalRequest>
GET
/{url-example}
host:{s3-name-example}.s3.{region-example}.amazonaws.com
x-amz-content-sha256:UNSIGNED-PAYLOAD
x-amz-date:20221123T001426Z
x-amz-security-token:{token-example}
x-amz-user-agent:aws-sdk-js/2.1248.0 callback promise
host;x-amz-content-sha256;x-amz-date;x-amz-security-token;x-amz-user-agent
UNSIGNED-PAYLOAD
</CanonicalRequest>
<CanonicalRequestBytes>...</CanonicalRequestBytes>
<RequestId>CKKWA23FWH266YRG</RequestId>
<HostId>JQtVWn6BC2qLF4/LJlzYUzzo6Rlus2yvy6FdOvA9lEr4QkBmzSRzXPcFEYAkXSRRuEFvjEnAR3YUR5qTIsqADw==</HostId>
</Error>
I'm guessing that if a client sends several requests to AWS in too short interval, it sends only a single response.
Is it right? Or is there another reason?

Related

Dart Signature V4 client Error: The request signature we calculated does not match the signature you provided

I am using the following code to connect AWS REST API using IAM authentication. It's working fine on Google Chrome but giving errors in Firefox.
import 'package:aws_signature_v4/aws_signature_v4.dart';
const signer = AWSSigV4Signer();
final scope = AWSCredentialScope(
region: 'ap-south-1',
service: AWSService.apiGatewayManagementApi,
);
final request = AWSHttpRequest(
method: AWSHttpMethod.post,
uri: Uri.https('<URL>', 'PATH'),
headers: const {
AWSHeaders.contentType: 'application/json;charset=utf-8',
},
body: json.encode(payload).codeUnits,
);
// Sign and send the HTTP request
final signedRequest = await signer.sign(
request,
credentialScope: scope,
);
final resp = await signedRequest.send();
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.
The Canonical String for this request should have been
'POST
<PATH>
content-type:application/json;charset=utf-8
host: <URL>
x-amz-content-sha256: <Key>
x-amz-date:20221017T065918Z
x-amz-user-agent:aws-sigv4-dart/0.2.2
content-type;host;x-amz-content-sha256;x-amz-date;x-amz-user-agent
<Key-1>
The String-to-Sign should have been
'AWS4-HMAC-SHA256
20221017T065918Z
20221017/ap-south-1/execute-api/aws4_request
<Key-2>
"
}
I am using docs from https://docs.amplify.aws/lib/project-setup/escape-hatch/q/platform/flutter/
I seen couple of question on similar issue with no answers.
Appreciate any help on this.

How to let API Gateway Proxy Error Responses?

I've got an API Gateway in front of a Lambda.
Successful responses from the Lambda (HTTP 2xx) have the response body forwarded.
However, for error responses (HTTP 5xx and others), the API Gateway transforms the response body using response templates.
Is there a way to avoid this? To have the original error response body from the Lambda?
In my Lambda I have this:
return callback(generalError, {
statusCode: 500,
headers:{
"content-type": "application/json"
},
body: JSON.stringify({
error: 'INTERNAL_ERROR',
description: error.message,
})
});
However, as output from the Gateway I get this:
{ "error": "Internal server error" }
Which doesn't match. The Lambdas response. It does match the response template in API Gateway:
{"message":$context.error.messageString}
However, is there a way to just proxy the original Lambda response instead of having this transformation in place?
I've found the reason why it doesn't work.
If you set a callback to a 500 error, including an object with an error field, somehow this will become the full response, regardless of the other output or real error.
Avoiding using an error field does the trick! I renamed it to serviceError and now I'm getting the responses I expected.

How I know the signature of a signed url?

I have a lambda which generates a signed URL for users to upload files to s3 bucket. The code is in nodejs:
export const getSignedURL = async (): Promise<{ signedURL: string }> => {
try {
const s3 = new S3();
const params = {
Bucket: CONFIG.s3Bucket,
Key: `${CONFIG.s3PictureFolder}/${uuidv4()}`,
Expires: CONFIG.presignedURLExpires,
};
const signedURL = await s3.getSignedUrlPromise('putObject', params);
console.log(`generate signedURL url: ${signedURL}`);
return { signedURL };
} catch (err) {
console.error(err);
throw err;
}
};
I am able to get the url success. However, when I test it via curl:
curl -XPUT PRESIGNED_URL --data "my data"
I got this error:
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><AWSAccessKeyId>XXXX</AWSAccessKeyId><StringToSign>
It seems that this URL requires an access key. Does this key mean AWS credential key which is issued by IAM?
This URL was generated by the lambda function. How do I know which key it uses? And I'd like to generate a public presigned url for users to upload. Is there a way to do that?
After come debugging, I found that I need to add Content-Type when generate the presigned URL like below:
const params = {
Bucket: CONFIG.s3Bucket,
Key: `${CONFIG.s3PictureFolder}/${uuidv4()}`,
Expires: CONFIG.presignedURLExpires,
ContentType: 'text/plain'
};

get parameters from aws lambda post request

I'm trying to receive a POST from a twilio widget, but my aws lambda (nodejs) function fails.
{"message": "Could not parse request body into json: Unrecognized token \'RecordingSource\': was expecting (\'true\', \'false\' or \'null\')\n at [Source: (byte[])\"RecordingSource=RecordVerb&RecordingSid=REd9475d9sdfw616e81995366d5f02291506b0&RecordingUrl=https%3A%2F%2Fapi.twilio.com%2F2010-04-01%2FAccounts%2FAC87e46c891a699385%2FRecordings%2FREd9466d5f02291506b0&RecordingStatus=completed&RecordingChannels=1&ErrorCode=0&CallSid=CA4a7f45753ef87894245dc95d445d8672&RecordingStartTime=Sat%2C%2021%20Mar%202020%2014%3A50%3A32%20%2B0000&AccountSid=AC8799385&RecordingDuration=2\"; line: 1, column: 17]"}
My AWS lambda function is very simple.
exports.handler = async (event) => {
console.log('-------------------------');
console.log(event);
console.log('-------------------------');
Your twilio widget is sending in application/x-www-form-urlencoded but your server is attempting to handle application/json. To send json instead is a client side configuration, and your widget will have to support such a configuration.
As #Alan suggests,
you could set the Recording Status Callback to a Twilio Function URL, which can do what you need it to do. Twilio Functions uses Node/JavaScript.

Custom response Lambda Authorizer for 401

Calling the Lambda callback function from a Lambda Authorizer with the string Unauthorized in the error parameter returns a 401 response with the body:
{ "message": "Unauthorized" }
Trying to use any other string in the response results in the response:
{ "message": null }
If instead you return a Deny Policy Document in the result parameter of the callback, you'll get a 403 with the response something like:
{ "message": "Unable to access resource with an explicit deny" }
After looking around it seems you need to configure a Gateway Response to return a custom response from a Lambda Authorizer, which I have working for the 403 response, but can't figure out how to do this for a 401.
For the 403 I created a Gateway Response with the template:
{\"message\":\"$context.authorizer.stringKey\"}
Then on the result object I set the following
ResultObject.context.stringKey = 'My custom response'
This works and is documented here.
However, for the 401, because I am not returning a policy document I don't know how to use a custom response. I created the same Gateway Response as I did for the 403, but if I hit the callback with any string (other than 'Unauthorized') in the error param I get the null message. I can't return in the result param because this needs to be a response structure containing the Policy Document.
Any ideas on how I can return a custom response with a 401?
Sorry to not answer your direct question, but I do think people (like me) might encounter this thread when looking on how to implement the first part of your question (return a 401 response from the authorizer lambda). You can follow AWS example here.
TL;DR:
For async functions, throw an error whose message exactly match the string "Unauthorized":
exports.handler = async function (event) {
...
throw Error("Unauthorized");
}
For sync. functions, call the callback function with its first parameter (the error response) exactly match the string "Unauthorized":
exports.handler = function(event, context, callback) {
..
callback("Unauthorized"); // Compared to a successful response `callback(null, ...)`
}
In both cases the response from the API gateway endpoint protected by your authorizer lambda would be:
401
{
"message": "Unauthorized"
}
You need to raise an exception, so when using node:
context.fail("Unauthorized");
For C# see http://yogivalani.com/aws-custom-lambda-authorizer-returns-401-unauthorized/