简体   繁体   中英

403 when upload file to S3 bucket using axios

I'm using axios to upload an audio file to AWS s3 bucket.

The workflow is: React => AWS API Gateway => Lambda.

Here is the backend Lambda code where generates the S3 presigned URL:

PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(AUDIO_S3_BUCKET)
                .key(objectKey)
                .contentType("audio/mpeg")
                .build();

        PutObjectPresignRequest putObjectPresignRequest = PutObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(10))
                .putObjectRequest(putObjectRequest)
                .build();

        PresignedPutObjectRequest presignedPutObjectRequest = s3Presigner.presignPutObject(putObjectPresignRequest);

        AwsProxyResponse awsProxyResponse = new AwsProxyResponse();
        awsProxyResponse.setStatusCode(HttpStatus.SC_OK);
        awsProxyResponse.setBody(
                GetS3PresignedUrlResponse.builder()
                        .s3PresignedUrl(presignedPutObjectRequest.url().toString())
                        .build().toString());
return awsProxyResponse;

Here is the java code to create the bucket:

    private void setBucketCorsSettings(@NonNull final String bucketName) {
        s3Client.putBucketCors(PutBucketCorsRequest.builder()
                .bucket(bucketName)
                .corsConfiguration(CORSConfiguration.builder()
                        .corsRules(CORSRule.builder()
                                .allowedHeaders("*")
                                .allowedMethods("GET", "PUT", "POST")
                                .allowedOrigins("*") // TODO: Replace with domain name
                                .exposeHeaders("ETag")
                                .maxAgeSeconds(3600)
                                .build())
                        .build())
                .build());
        log.info("Set bucket CORS settings successfully for bucketName={}.", bucketName);
    }

In my frontend, here is the part that try to upload file:

  const uploadFile = (s3PresignedUrl: string, file: File) => {
    let formData = new FormData();
    formData.append("file", file);
    formData.append('Content-Type', file.type);
    const config = {
        headers: {
          "Content-Type": 'multipart/form-data; boundary=---daba-boundary---'
          //"Content-Type": file.type,
        },
        onUploadProgress: (progressEvent: { loaded: any; total: any; }) => {
            const { loaded, total } = progressEvent;

            let percent = Math.floor((loaded * 100) / total);

            if (percent < 100) {
                setUploadPercentage(percent);
            }
        },
        cancelToken: new axios.CancelToken(
            cancel => (cancelFileUpload.current = cancel)
        )
    };

    axios(
          { 
            method: 'post', 
            url: s3PresignedUrl, 
            data: formData,
            headers: {
              "Content-Type": 'multipart/form-data; boundary=---daba-boundary---'
            }
          }
        )
        .then(res => {
            console.log(res);
            setUploadPercentage(100);

            setTimeout(() => {
                setUploadPercentage(0);
            }, 1000);
        })
        .catch(err => {
            console.log(err);

            if (axios.isCancel(err)) {
                alert(err.message);
            }
            setUploadPercentage(0);
        });
  };

However, when try to upload the file, it return 403 error.

And if I use fetch instead of axios instead and it works, like this:

export async function putToS3(presignedUrl: string, fileObject: any) {
  const requestOptions = {
    method: "PUT",
    headers: {
      "Content-Type": fileObject.type,
    },
    body: fileObject,
  };
  //console.log(presignedUrl);
  const response = await fetch(presignedUrl, requestOptions);
 //console.log(response);
  return await response;
}

putToS3(getPresignedUrlResponse['s3PresignedUrl'], values.selectdFile).then(
          (putToS3Response) => {
            console.log(putToS3Response);
            Toast("Success!!", "File has been uploaded.", "success");
          }  
        );

It seems to me that the only difference between these two is that: when using fetch the request's Content-Type header is Content-Type: audio/mpeg , but when using axios it is Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryClLJS3r5Xetv3rN7 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryClLJS3r5Xetv3rN7 .

How can I make it work with axios? I'm switching to axios for its ability to monitor request progress as I want to show an upload progress bar.

I followed this blog and not sure what I missed: https://bobbyhadz.com/blog/aws-s3-presigned-url-react

You didn't mark any answers as accepted so I guess you didn't solve it.

For any future viewers out there. The reason why you are getting 403 forbidden error is because your Content-Type in your server and client side are not matching. I'm assuming you set up the AWS policies correctly.

Your code in the backend should look like this:

const presignedPUTURL = s3.getSignedUrl("putObject", {
  Bucket: "bucket-name",
  Key: String(Date.now()),
  Expires: 100,
  ContentType: "image/png", // important
});

and in the front-end (assuming you are using axios):

const file = e.target.files[0]

const result = await axios.put(url, file, {
  withCredentials: true,
  headers: { "Content-Type": "image/png" },
});

In practical, you would normally have to send the file type to generate the pre-signed url in the POST body or whatever and then in axios you do file.type to get the file type of the uploaded file.

You are using POST in your axios. Should be PUT instead.

Also I think the content type has to match the one specified during requesting the pre-signed URL, which is audio/mpeg as you rightly pointed out.

Correspondingly, your data should be just file , instead of formData .

axios(
          { 
            method: 'put', 
            url: s3PresignedUrl, 
            data: file,
            headers: {
              "Content-Type": 'audio/mpeg'
            }
          }
...

Check your Lambda execution role. It may be the culprit. Perhaps it does not grant enough permissions to allow PUTting files into your bucket.

URL signing is a delegation of power on behalf of the signer, which is restricted to a specified object, action... Signing does not magically grants full read/write permissions on S3, even on the specific object related to the presigned URL.

The "user" who generates the signature requires sufficient permissions to allow the actions you want to delegate through that presigned URL. In this case, this is the execution role of your Lambda function.

You can add the AmazonS3FullAccess managed policy to the execution role and see if it solves your situation. This change took me out of a blocked situation me after days of struggle. Afterwards, before going to production, restrict that rule to the specific bucket you want to allow uploads into (least privilege principle).

If you develop using SAM local emulation, those execution roles seem not to be taken into account as long as you run your functions locally; the signed links work in that context even without S3 permissions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM