After successfully obtaining a presigned URL through my Node/Express backend for a putObject
request on S3, I attempt to upload the relevant files via the browser.
However, when making the put
request through the browser (or Postman), I encounter a 400
or 403
error. As someone new to S3, I am unsure where to find information regarding this issue.
While I am able to acquire the presigned URL correctly, why does my put
request for the files associated with the URL fail?
To my knowledge, the bucket that I make requests for is publicly accessible.
I have researched various code walk-through tutorials, S3 documentation, and posts related to this topic.
The frontend code used to request presigned URLs is as follows:
// Returns null or an array of objects in the form of { url : string | null; contentType: string; }
const filesDescription = fileList[+key];
if (filesDescription === undefined || filesDescription === null) continue;
const Key = `${prefix}/${filesDescription.name}`;
const ContentType = filesDescription.type;
const request = generateEndpointConfig(APIEndpoints.GET_PRESIGNED_URL, { Key, ContentType, userID });
const res = await apiCall(request.METHOD, request.URL, request.DATA);
const url = typeof res.data.signedUrl === "string" ? res.data.signedUrl : null;
presignedUrls.push({url, contentType: ContentType});
The backend Node/Express code used to obtain the URL is as follows:
const { ContentType, Key } = req.body;
const s3 = new S3({
accessKeyId: AWS_ACCESS_ID,
secretAccessKey: AWS_SECRET
});
const url = await s3.getSignedUrlPromise("putObject",
{
Bucket: AMAZON_AWS_STATIC_BUCKET,
ContentType,
Key,
Expires: 300
});
return res.status(StatusCodes.OK).json({ signedUrl: url })
Lastly, the put
requests made to upload files are as follows:
const presignedUrls = await getPresignedUrls(+idx);
if (presignedUrls === null) return false;
for (const fileIdx in presignedUrls)
{
const fileList = files[idx];
const uploadConfig = presignedUrls[fileIdx];
if (fileList === null || uploadConfig.url === null) continue;
const fileToUpload = fileList[fileIdx];
try
{
// Make put request for corresponding file to cloud service
await axios.put(uploadConfig.url, fileToUpload,
{
headers:
{
"Content-Type": uploadConfig.contentType
}
});
}
catch(err)
{
return false;
}
It's also worth mentioning that since this is for authenticated users, an Authorization
header in the form of Bearer <TOKEN>
is transmitted.
=== Edit ===
This is a sample error response received with status code 403 in Postman:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>...</RequestId>
<HostId>...</HostId>
</Error>
=== Edit 2 ===
The policy set on the bucket includes:
{
"Version": "2012-10-17",
"Id": "<POLICY_ID>",
"Statement": [
{
"Sid": "<STMT_ID>",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <CLOUDFRONT_ORIGIN_ID>"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<S3_BUCKET_NAME>/*"
},
{
"Sid": "<STMT_ID>",
"Effect": "Allow",
"Principal": {
"Federated": [
"http://localhost:3000",
"https://www.my-website.com/"
],
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::<S3_BUCKET_NAME>/*"
}
]
}
The CORS configuration on the bucket is as follows:
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"https://www.my-website.com",
"http://localhost:3000"
],
"ExposeHeaders": []
}
]
For clarification, this bucket serves a CloudFront distribution for a CDN network, and access to the bucket is restricted only for GET requests at the domain level.
=== Edit 3 ===
An error occurs during the upload process in the browser:
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature provided by you. Check your key and signing method.</Message>
<AWSAccessKeyId>...</AWSAccessKeyId>
<StringToSign>GET [A_LONG_NUMBER] /[BUCKET_NAME]/[PREFIX]/[IMAGE_NAME].jpg</StringToSign>
<SignatureProvided>...</SignatureProvided>
<StringToSignBytes>[SOME_BYTES]</StringToSignBytes>
<RequestId>...</RequestId>
<HostId>...</HostId>
</Error>
=== Final Edit ===
Alongside the solution provided by @jarmod, it was discovered that my application config automatically adds an Authorization
header to all requests once users are authenticated on the site. Removing this header for calls to S3 resolved the issue successfully in the browser.