Richbits
Richbits

Reputation: 7624

POST s3 presigned url call in postman returns SignatureDoesNotMatch

When calling a presigned POST endpoint returned from my node.js server I receive what looks like a valid response:

GET presigned url Note I am setting environment variables to make the POST call easier to repeat.

When calling the presigned URL however I receive a 403 response with SignatureDoesNotMatch error code. I am having a lot of difficulty finding resources about using the POST presigned url. So my questions are as follows:

  1. Am I calling the presigned URL correctly (see below)?
  2. How can I debug to understand what is happening here (I've tried turning on the bucket logs, but nothing is logged)?
  3. What might I look at further to resolve this issue?

When performing the post it is my understanding that the call is to POST to the url returned and using form-data include all "fields" in the body with the final key being labelled as file with the file to be uploaded attached. From the above response I have therefore used the following call in postman: Headers: POST presigned url Headers

Body (I have tried both with and without Content-Type included): body

However when I make this call I receive a 403 forbidden response and a SignatureDoesNotMatch error code:
403 response

Here is the code to generate the presigned URL (using "@aws-sdk/client-s3": "^3.25.0", "@aws-sdk/s3-presigned-post": "^3.25.0" within package.json):

const { createPresignedPost } = require("@aws-sdk/s3-presigned-post");
const { S3Client } = require("@aws-sdk/client-s3");

const s3 = new S3Client({
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
  },
  signatureVersion: "v4",
  region: "eu-west-2",
});
async function getSignedUrl() {

  const params = {
    Bucket: "richbits-test",
    Key: "d3c0c9a0-ff91-11eb-bbe6-b9d90cd8bb8f",
    Conditions: [["eq", "$Content-Type", "image/jpeg"]],
  };

  console.log(params);

  const signedUrl = await createPresignedPost(s3, params);

  return signedUrl;
}

I've double checked the bucket and keys, but would appreciate some advice as to how I might move forward or any useful resources which can help me understand the use of these presigned urls better please.

Upvotes: 3

Views: 1631

Answers (1)

ogdenkev
ogdenkev

Reputation: 2374

Woah, the documentation from AWS on these headers is terrible. Here is what I figured out.

  1. The output of createPresignedPost() is an object with two keys: url and fields. The fields object contains all the form fields and respective values that you must use when submitting the post. You would copy these fields and only these fields to your Postman request.
  2. There are a few fields that the AWS SDK will add automatically, including bucket, key, policy, and several fields required to generate the signature. With the JavaScript SDK, these were X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, and X-Amz-Signature (though for the Python SDK, these were AWSAccessKeyId and signature). These will be returned in the fields key from createPresignedPost(), so you do not need to manually generate them.
  3. The Fields parameter of createPresignedPost() is for additional fields that you want to add to the form. The entire set of fields that could possibly be added is listed here. However, the SDK handles many of these for you, particularly the required ones. According to the Python SDK docs, the fields that you could include in Fields are acl, Cache-Control, Content-Type, Content-Disposition, Content-Encoding, Expires, success_action_redirect, redirect, success_action_status, and x-amz-meta-.
  4. The Fields and Conditions parameters of createPresignedPost() need to be synchronized such that any field in Fields also has a condition in Conditions and vice versa.

Putting this altogether, I believe the problem with your getSignedUrl() function was that it was missing a Fields parameter for createPresignedPost() that had a key-value pair for Content-Type (since you have a condition for Content-Type). Without the Content-Type in the Fields parameter, the calculated signature would not include the Content-Type field. If you then left out the Content-Type field your signature might match, but your policy conditions would fail because it required Content-Type to be present and equal to image/jpeg. If you included Content-Type, then your signature would not match.

Here is could that should work for you.

const { createPresignedPost } = require("@aws-sdk/s3-presigned-post");
const { S3Client } = require("@aws-sdk/client-s3");

const s3 = new S3Client({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  },
  signatureVersion: "v4",
  region: "eu-west-2",
});
async function getSignedUrl() {

  const params = {
    Bucket: "richbits-test",
    Key: "d3c0c9a0-ff91-11eb-bbe6-b9d90cd8bb8f",
    Conditions: [["eq", "$Content-Type", "image/jpeg"]],
    Fields: {"Content-Type": "image/jpeg"},
  };

  console.log(params);

  const signedUrl = await createPresignedPost(s3, params);

  return signedUrl;
}
getSignedUrl().then(res => {
        console.log(res)
})

The output of this should be

{
  Bucket: 'richbits-test',
  Key: 'd3c0c9a0-ff91-11eb-bbe6-b9d90cd8bb8f',
  Conditions: [ [ 'eq', '$Content-Type', 'image/jpeg' ] ],
  Fields: { 'Content-Type': 'image/jpeg' }
}
{
  url: 'https://s3.eu-west-2.amazonaws.com/richbits-test',
  fields: {
    'Content-Type': 'image/jpeg',
    bucket: 'richbits-test',
    'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
    'X-Amz-Credential': '<YOUR_ACCESS_KEY_ID>/20211005/eu-west-2/s3/aws4_request',
    'X-Amz-Date': '20211005T111446Z',
    key: 'd3c0c9a0-ff91-11eb-bbe6-b9d90cd8bb8f',
    Policy: '<SOME_BASE64_ENCODED_STRING>',
    'X-Amz-Signature': '<THE_SIGNATURE>'
  }
}

Then your POST request would include the form fields Content-Type, bucket, X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, Policy, and X-Amz-Signature. Here is what an HTML form would look like.

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>

  <form action="https://s3.eu-west-2.amazonaws.com/richbits-test" method="post" enctype="multipart/form-data">
    Bucket:
    <input type="input" name="bucket" value="richbits-test" /><br />
    Key to upload: 
    <input type="input"  name="key" value="d3c0c9a0-ff91-11eb-bbe6-b9d90cd8bb8f" /><br />
    Content-Type: 
    <input type="input"  name="Content-Type" value="image/jpeg" /><br />
    <input type="text"   name="X-Amz-Credential" value="<YOUR_ACCESS_KEY_ID>/20211005/eu-west-2/s3/aws4_request" />
    <input type="text"   name="X-Amz-Date" value="20211005T111446Z" />

    <input type="hidden" name="Policy" value="<SOME_BASE64_ENCODED_STRING>" />'
    <input type="hidden" name="X-Amz-Algorithm" value="AWS4-HMAC-SHA256" />
    <input type="hidden" name="X-Amz-Signature" value="<THE_SIGNATURE>" />
    
    File: 
    <input type="file"   name="file" /> <br />
    <!-- The elements after this will be ignored -->
    <input type="submit" name="submit" value="Upload to Amazon S3" />
  </form>
  
</html>

As far as debugging this issue, I did not find anything useful from the POST request to S3. I either got no response or an unauthorized status, without any indication what the problem was.

Upvotes: 5

Related Questions