5

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.

Keith Harris
  • 1,118
  • 3
  • 13
  • 25
  • 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 Jan 30 '20 at 01:11
  • 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 Jan 30 '20 at 01:15

5 Answers5

4

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' });
mozu
  • 729
  • 1
  • 7
  • 10
2

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.

Arun Kamalanathan
  • 8,107
  • 4
  • 23
  • 39
0

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>
  • Oh, I forgot to mention that I already had the CORS configuration policy set up in my bucket. That was such an obvious thing that I didn't think to mention it. – Keith Harris Jan 29 '20 at 18:16
  • Just to be sure, first check if your file type is not wrong. As I see you're naming it with .jpg but content-type is jpeg If that doesnt help, try to do this request to upload: axios.put(presignedUrl, null, file, { isMultipartForm: false, headers: { 'Content-Type': file.type } }); Hope it helps ;) – Fabiano Soder Jan 29 '20 at 18:30
  • Another thing that may helps is generate presignedUrl and uploadFile all by client side – Fabiano Soder Jan 29 '20 at 18:32
  • const result = await axios.put(response.data.uploadURL, null, blobData, { isMultipartForm: false, headers: { 'Content-Type': blobData.type } }); – Keith Harris Jan 29 '20 at 18:43
  • 1
    You suggested generating the presignedUrl client side. How can I do that without revealing the AWS Access and Secret Keys? – Keith Harris Jan 29 '20 at 18:45
  • 1
    There's no need to create the pre-signed URL client-side. That's a red herring, and generally a bad security practice. – jarmod Jan 30 '20 at 01:12
0

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)
Joseph
  • 432
  • 4
  • 8
-1

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/

janquijano
  • 144
  • 4