2
votes

I'm trying to implement a picture upload functionality for a Vue app using AWS S3 a pre-signed URL. The first step is to send a request to an API that will create the signed URL to upload the file. This part works fine:

Server side:

'use strict';

const aws = require('aws-sdk');
const config = require('../config');
const util = require('./util');
const uuidv4 = require('uuid/v4');

const bucketName = 'myAmazonS3Bucket';

aws.config.update({
  secretAccessKey: config.AWS_SECRET_ACCESS_KEY,
  accessKeyId: config.AWS_ACCESS_KEY_ID,
  region: 'us-west-2'
});

const s3 = new aws.S3({ signatureVersion: 'v4' });

const handler = async (event) => {
    console.log('Uploading file...');

    return await getUploadURL();
}

const getUploadURL = async () => {
    const actionId = uuidv4();

    const s3Params = {
      Bucket: bucketName,
      Key:  `${actionId}.jpg`,
      ContentType: 'image/jpeg',
      ACL: 'public-read'
    };

    console.log(s3Params);

    return new Promise((resolve, reject) => {
        let uploadURL = s3.getSignedUrl('putObject', s3Params);

        console.log(uploadURL);

        resolve({
            "statusCode": 200,
            "isBase64Encoded": false,
            "headers": { "Access-Control-Allow-Origin": "*" },
            "body": JSON.stringify({
                "uploadURL": uploadURL,
                "photoFilename": `${actionId}.jpg`
            })
        });

        reject({
            "statusCode": 500,
            "headers": { "Access-Control-Allow-Origin": "*" },
            "body": "A funky error occurred and I am not happy about it!"
        })
    });
}

module.exports = {
    handler
}

The API endpoint sends a response similar to this one:

{
    "uploadURL": "https://s3.us-west-2.amazonaws.com/pics.amazon-clone.io/7925d452-cadd-4f06-ba63-cc50645e3cfb.jpg?AWSAccessKeyId=AKIASGDJJ5ZLUVPMUYMQ&Content-Type=image%2Fjpeg&Expires=1580276753&Signature=3rqNckP4DiL6DkWPRuEGJsuIGpw%3D&x-amz-acl=public-read",
    "photoFilename": "7925d452-cadd-4f06-ba63-cc50645e3cfb.jpg"
}

The client will use the uploadUrl to upload the file to the S3 bucket. Here is the client code for that:

uploadImage: async function (e) {
            console.log('Upload clicked')
            console.log(e)

            // Get the presigned URL
            const response = await axios({
                method: 'POST',
                url: API_ENDPOINT
            })
            console.log('Response: ', response.data)
            console.log('Uploading: ', this.image)

            let binary = atob(this.image.split(',')[1])
            let array = []
            for (var i = 0; i < binary.length; i++) {
                array.push(binary.charCodeAt(i))
            }
            let blobData = new Blob([new Uint8Array(array)], {type: 'image/jpeg'})
            console.log('Uploading to: ', response.data.uploadURL)
            const result = await fetch(response.data.uploadURL, {
                method: 'PUT',
                headers: { 
                    'Content-Type': 'image/jpeg', 
                    'x-amz-acl': 'public-read' },
                body: blobData
            })
            console.log('Result: ', result)
            // Final URL for the user doesn't need the query string params
            this.uploadURL = response.data.uploadURL.split('?')[0]
        }

Unfortunately, I'm getting a forbidden 403 error when using the signed URL. Here is the result of the console errors I get from my Chrome browser:

Uploading to: https://s3.us-west-2.amazonaws.com/pics.amazon-clone.io/b1bdb5e3-7f64-49f7-b779-11b3f67317ee.jpg?Content-Type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASGDJJ5ZLUVPMUYMQ%2F20200129%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20200129T165522Z&X-Amz-Expires=900&X-Amz-Signature=b230c9a40065585307e150655466bbab3d0d99aa43f8620377ab977eb1c7234c&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read

Pic.vue?937b:60 OPTIONS https://s3.us-west-2.amazonaws.com/pics.amazon-clone.io/b1bdb5e3-7f64-49f7-b779-11b3f67317ee.jpg?Content-Type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASGDJJ5ZLUVPMUYMQ%2F20200129%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20200129T165522Z&X-Amz-Expires=900&X-Amz-Signature=b230c9a40065585307e150655466bbab3d0d99aa43f8620377ab977eb1c7234c&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read 403 (Forbidden)

Access to fetch at 'https://s3.us-west-2.amazonaws.com/pics.amazon-clone.io/b1bdb5e3-7f64-49f7-b779-11b3f67317ee.jpg?Content-Type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASGDJJ5ZLUVPMUYMQ%2F20200129%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20200129T165522Z&X-Amz-Expires=900&X-Amz-Signature=b230c9a40065585307e150655466bbab3d0d99aa43f8620377ab977eb1c7234c&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

5
If this is a Lambda function, you should strongly consider leveraging an IAM role rather than supplying hard-coded AWS credentials in a config file that you package and upload with the Lambda function.jarmod
Presumably your target S3 bucket allows uploading of objects with the public-read canned ACL. And your client is using the pre-signed URL before it expires and before the credentials that you used to create the pre-signed URL expire.jarmod

5 Answers

1
votes

I also had 403 error.

In my case, I just set signatureVersion, and solved it.

before

const s3 = new aws.S3();

after

const s3 = new aws.S3({ signatureVersion: 'v4' });
0
votes

When you get a OPTIONS -> CORS Policy error from AWS S3 Buckets it means that your bucket configuration are not allowing you to upload images.

To fix that you must update your bucket's permissions configurations.

To do that go to your bucket -> permissions tab -> CORS tab

Then put the following code into there:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin><YOUR_DOMAIN></AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Dont forget to replace <YOUR_DOMAIN> with your domain, obviously :P

If you want to allow all domains just replace this line

<AllowedOrigin><YOUR_DOMAIN></AllowedOrigin> 

with this

<AllowedOrigin>*</AllowedOrigin>
0
votes

here is how I would troubleshoot. I would first confirm the generated URL is working using some tools such as curl or postman, then I would inspect the request sent to s3 and look for any additional headers present in the request.

Step 1 - confirm the generated URL is working

I would try uploading via curl/postman to see if the URL is working.

curl -X PUT -T ~/Downloads/car.jpg https://s3.us-west-2.amazonaws.com/pics.amazon-clone.io/b1bdb5e3-7f64-49f7-b779-11b3f67317ee.jpg?Content-Type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIASGDJJ5ZLUVPMUYMQ%2F20200129%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20200129T165522Z&X-Amz-Expires=900&X-Amz-Signature=b230c9a40065585307e150655466bbab3d0d99aa43f8620377ab977eb1c7234c&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read

Step 2 - confirm whether reactjs fetch request is working correctly

If you can confirm that the step1 is working, then you can check the actual headers being sent with the request using the browser's network tab. see if the code is sending any additional headers without your knowledge.

Hope this helps.

0
votes

If you have a CORS issue, try uploading images from curl instead of your frontend application because the issue may be header related. For example: curl -v --upload-file "image.png" "https://s3.your-presigned-url". If your frontend sends a "Content-type" header, you need to include the "Content-Type" when your link is being generated. This is the reference I found for my solution

Here is my working revised python code which solved my problem:

response = s3_client.generate_presigned_url('put_object',
                                                Params={'Bucket': bucket_name,
                                                        'Key': object_name,
                                                        'ContentType':"image/png"}, #DONT FORGET THIS
                                                ExpiresIn=expiration)
0
votes

The error is obviously due to CORS. Hope you don't mind but I'll instead suggest a better pattern to do the same use-case. If you are open to using API Gateway, it's very easy to setup and more secure than using pre-signed url. You also decouple your front end code from the upload/download logic.

Have a read at this: https://aws.amazon.com/premiumsupport/knowledge-center/api-gateway-upload-image-s3/