Getting 403 Forbidden when trying to upload file to AWS S3 with presigned post using Boto3 (Django + Javascript)

Viewed 998

I've tried researching other threads here on SO and other forums, but still can't overcome this issue. I'm generating a presigned post to S3 and trying to upload a file to it using these headers, but getting a 403: Forbidden.

Permissions The IAM user loaded in with Boto3 has permissions to list, read and write to S3.

IAM permissions

CORS CORS from all origins and all headers are allowed

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

The code The code is based on Python in Django as well as Javascript. This is the logic:

First the file is retrieved from the HTML, and used to call a function for retrieving the signed URL.

(function () {
    document.getElementById("file-input").onchange = function () {
        let files = document.getElementById("file-input").files;
        let file = files[0];
        Object.defineProperty(file, "name", {
            writeable: true,
            value: `${uuidv4()}.pdf`

        })
        if (!file) {
            return alert("No file selected");
        }
        getSignedRequest(file);
    }
})();

Then a GET request is sent to retrieve the signed URL, using a Django view (described in the next section after this one)

function getSignedRequest(file) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "/sign_s3?file_name=" + file.name + "&file_type=" + file.type)
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                let response = JSON.parse(xhr.responseText)
                uploadFile(file, response.data, response.url)
            }
            else {
                alert("Could not get signed URL")
            }
        }
    };
    xhr.send()
}

The Django view generating the signed URL

def Sign_s3(request):

    S3_BUCKET = os.environ.get("BUCKET_NAME")

    if (request.method == "GET"):
        file_name = request.GET.get('file_name')
        file_type = request.GET.get('file_type')
        
        s3 = boto3.client('s3', config = boto3.session.Config(signature_version = 's3v4'))
        
        presigned_post = s3.generate_presigned_post(
        Bucket = S3_BUCKET,
        Key = file_name,
        Fields = {"acl": "public-read", "Content-Type": file_type},
        Conditions = [
        {"acl": "public-read"},
        {"Content-Type": file_type}
        ],
        ExpiresIn = 3600
    )


    return JsonResponse({
        "data": presigned_post,
        "url": "https://%s.s3.amazonaws.com/%s" % (S3_BUCKET, file_name)
    })

Finally the file should be uploaded to the bucket (this is where I'm getting the 403 error)

function uploadFile(file, s3Data, url) {
            let xhr = new XMLHttpRequest();
            xhr.open("POST", s3Data.url)

            let postData = new FormData()
            for (key in s3Data.fields) {
                postData.append(key, s3Data.fields[key])
            }

            postData.append("file", file)

            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200 || xhr.status === 204) {
                        document.getElementById("cv-url").value = url
                    }
                    else {
                        alert("Could not upload file")
                    }
                }
            };
            xhr.send(postData)
        }

The network request This is how the network request looks in the browser

Network request 1 Network request 2 Network request 3

2 Answers

@jellycsc helped me. I had to open up the BlockPublicAcl option for the bucket for it to work.

The URL that you should be using in the upload is supposed to be the one that the presigned response has. Don't just upload whatever url you want.

Update your response to be:

return JsonResponse({
  "data": presigned_post,
  "url": presigned_post.url
})

Specifically the url you are using looks like:

https://BUCKTET_NAME.s3.amazonaws.com/KEY_PATH

When it should look like:

https://s3.REGION.amazonaws.com/BUCKET_NAME

However looking at your code this is what it should be doing, but your screen shot from inspector says otherwise. Why does the url in the network request NOT match the url that was returned by the create_presigned_post request?

Related