Sid
Sid

Reputation: 21

S3: Put request to using a pre-signed URL resulting in 403 (Forbidden)

I set up a S3 bucket {bucketName} and an IAM user, {iamUsername}. The user has the following policies to access the bucket:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::{bucketName}",
                "arn:aws:s3:::{bucketName}/*"
            ]
        }
    ]
}

And the bucket has the following bucket policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::012345678901:user/iamUsername"
            },
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::bucketName",
                "arn:aws:s3:::bucketName/*"
            ]
        }
    ]
}

Other than this, the bucket blocks all public access as recommended by AWS. I use the access key of the user to pre-sign the URL to upload a file in Flask:

s3 = boto3.client('s3', aws_access_key_id=AWS_ACCESS_KEY_ID,
                          aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
                          region_name=AWS_REGION)
presigned_url = s3.generate_presigned_url(
            'put_object',
            Params={
                'Bucket': S3_BUCKET,
                'Key': random_file_name,
                'ContentType': file_type
            },
            ExpiresIn=3600  # URL expires in 1 hour
        )

I use this signed url to upload the file in react:

const uploadFile = async (signedUrl) => {
    const uploadFormData = new FormData();
    uploadFormData.append('file', selectedFile);

    try {
        await fetch(signedUrl, {
          method: 'PUT',
          body: uploadFormData
        })
        .then(response => {
          console.log(response);
          if (!response.ok) {
            console.log(response);
            throw new Error(response);
          }
          return response.json();
        })

This is resulting in a 403, like below: 403 error

body: ReadableStream
bodyUsed: false
headers: Headers {}
ok: false
redirected: false
status: 403
statusText: "Forbidden"
type: "cors"
url: "https://{bucketName}.s3.amazonaws.com/{fileName}?AWSAccessKeyId={key}&Signature={signature}&content-type=image%2Fjpeg&Expires=1709497547"

Would appreciate help in figuring out what is wrong with the approach, thanks!

PS: I have also set up CORS for my bucket as-

    [
        {
            "AllowedHeaders": [
                "*"
            ],
            "AllowedMethods": [
                "GET",
                "PUT",
                "DELETE",
                "HEAD",
                "POST"
            ],
            "AllowedOrigins": [
                "*"
            ],
            "ExposeHeaders": [],
            "MaxAgeSeconds": 3600
        }
    ]

Upvotes: 2

Views: 714

Answers (1)

jarmod
jarmod

Reputation: 78583

The problem is that you created a pre-signed URL that includes a Content-Type header but you didn't indicate that header later when you used the URL.

If you include HTTP headers at the time you pre-sign the URL then those headers are included in the signing calculation. You need to make sure that the same HTTP headers are sent to S3 in the subsequent HTTP request.

So, when calling fetch, include the matching content type e.g.

headers: { "Content-Type": "image/png" }

For more pre-signed URL troubleshooting, see here.

Upvotes: 1

Related Questions