aws / aws-sdk-js-v3

Modularized AWS SDK for JavaScript.
Apache License 2.0
3.08k stars 575 forks source link

Adding ContentType to S3Client's 'PutObjectCommand' gives 'SignatureDoesNotMatch' Error #5268

Open henryson opened 1 year ago

henryson commented 1 year ago

Checkboxes for prior research

Describe the bug

Looks like the exact same issue as in: https://github.com/aws/aws-sdk-js-v3/issues/1916 but I still have the same problem. (The page asked me to create a new issue).

My versions:

"@aws-sdk/client-s3": "^3.420.0", "@aws-sdk/lib-storage": "^3.420.0", "@aws-sdk/s3-request-presigner": "^3.420.0"

My code:

import cuid from 'cuid'
import { S3, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import config from "../config/config"

const client = new S3({
    region: "eu-north-1",
    endpoint: config.myUrl,
    credentials: {
        accessKeyId: config.myAccess,
        secretAccessKey: config.mySecret,
    }
})

const signedUrl = async (args: any) => {
    const fileId = cuid()
    const filetype = args.filetype
    const filename = args.filename
        .replace(/([Ì]|[^0-9a-öA-Ö.\s])/g, '')
        .normalize('NFKD')
        .replace(/([\u0300-\u036f]|[^0-9a-zA-Z.\s])/g, '')
    const command = new PutObjectCommand({ Key: fileId + "/" + filename, Bucket: config.myBucket, ContentType: filetype, ContentLength: 1 })
    const url = await getSignedUrl(client, command, { expiresIn: 15 * 60 })
    return { id: fileId, name: filename, url, type: filetype }
}

From signed URL:

X-Amz-SignedHeaders | content-length;host

I do not get it how this can have been fixed in 3.3.0? Is it different when using the presigned URL API?

Thanks in advance for any help!

Cheers, Josef

SDK version number

"@aws-sdk/client-s3": "^3.420.0", "@aws-sdk/lib-storage": "^3.420.0", "@aws-sdk/s3-request-presigner": "^3.420.0"

Which JavaScript Runtime is this issue in?

Node.js

Details of the browser/Node.js/ReactNative version

node 18.12.1

Reproduction Steps

import cuid from 'cuid'
import { S3, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import config from "../config/config"

const client = new S3({
    region: "eu-north-1",
    endpoint: config.myUrl,
    credentials: {
        accessKeyId: config.myAccess,
        secretAccessKey: config.mySecret,
    }
})

const signedUrl = async (args: any) => {
    const fileId = cuid()
    const filetype = args.filetype
    const filename = args.filename
        .replace(/([Ì]|[^0-9a-öA-Ö.\s])/g, '')
        .normalize('NFKD')
        .replace(/([\u0300-\u036f]|[^0-9a-zA-Z.\s])/g, '')
    const command = new PutObjectCommand({ Key: fileId + "/" + filename, Bucket: config.myBucket, ContentType: filetype, ContentLength: 1 })
    const url = await getSignedUrl(client, command, { expiresIn: 15 * 60 })
    return { id: fileId, name: filename, url, type: filetype }
}

Observed Behavior

X-Amz-SignedHeaders | content-length;host (from presigned URL)

Expected Behavior

X-Amz-SignedHeaders | content-length;host;content-type

Possible Solution

No response

Additional Information/Context

No response

kevado commented 1 year ago

I'm getting this error while using SNS and I'm not adding any HTTP headers.

yenfryherrerafeliz commented 1 year ago

Hi @henryson, while I investigate why this behavior happens, could you please try to explicitly mark "content-type" as a signable header as follow:

const url = await getSignedUrl(client, command, { expiresIn: 15 * 60, signableHeaders: new Set(["content-type"]) })

the full code would be:

import cuid from 'cuid'
import { S3, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import config from "../config/config"

const client = new S3({
    region: "eu-north-1",
    endpoint: config.myUrl,
    credentials: {
        accessKeyId: config.myAccess,
        secretAccessKey: config.mySecret,
    }
})

const signedUrl = async (args: any) => {
    const fileId = cuid()
    const filetype = args.filetype
    const filename = args.filename
        .replace(/([Ì]|[^0-9a-öA-Ö.\s])/g, '')
        .normalize('NFKD')
        .replace(/([\u0300-\u036f]|[^0-9a-zA-Z.\s])/g, '')
    const command = new PutObjectCommand({ Key: fileId + "/" + filename, Bucket: config.myBucket, ContentType: filetype, ContentLength: 1 })
    const url = await getSignedUrl(client, command, { expiresIn: 15 * 60, signableHeaders: new Set(["content-type"]) })
    return { id: fileId, name: filename, url, type: filetype }
}

Please make sure that this header is provide when executing the request.

Let me know if that helps!

Thanks!

henryson commented 1 year ago

Hi Yenfry and thank you for your response!

I have tried your code above and I can confirm that the "content-type" header is now returned in the signed URL.

But I still get the CORS error :-(

Have tried both uppercase and lowercase header on both creating the signed URL and calling it.

I can see in my call that Axios adds an "Accept" header as well (see image). Tried to include that in signedHeaders as well, but it does not show up. I also tried to delete the Accept header from the axios call without luck. Tried both put and post.

image

henryson commented 1 year ago

I realize that your answer solves this issue since I get the content-type back in the signed url query parameter.

But what does signableHeaders mean here? Do I have to sign/encrypt the header before making the request to the signed URL?

yenfryherrerafeliz commented 1 year ago

Hi @henryson, signableHeaders is an option to explicitly tell to the signing process which headers you want to be include as signed, and when those headers are not added by the signing process itself.

Please let me know if that clarifies your question.

Thanks!

henryson commented 1 year ago

Hi @yenfryherrerafeliz and thank you very much for your help!

I have added the signableHeaders when I produce the signed URL. But I still get 403 in the preflight request.

This works:

  const upload = await axios.put(signedUrl.url, file)

This do not work:

  const upload = await axios.put(signedUrl.url, file, { headers: { "Content-type": file.type } })

The strange thing is that if I do not include headers, it seems axios (or browser?) still send Content-Type header but with the value : "application/x-www-form-urlencoded" It just hit me though that the above maybe just applies to the put request and not the file itself? I may have go back to school and learn how to set the content type to the file?

However, maybe you could tell me anyway how this is expected to work. Should I include "content-type" in the command I send with "getSignedUrl" or not? Do I have to do anything when uploading the file, or is it sufficient with the parameters in the signed URL?

nodegin commented 11 months ago

I have the same issue so I downgraded to the aws-sdk package which works perfectly for me:

  const signedUrl = await s3.getSignedUrlPromise('putObject', {
    Bucket: bucket,
    Key: key.join('/'),
    Expires: 60 * 10,
    ContentType: mimeType,
    ACL: 'public-read',
  });
dsiah-aloft commented 9 months ago

@yenfryherrerafeliz did you tag this workaround-available based on your answer? I was also unable to use that to solve my SignatureDoesNotMatch error. The only solution I have so far is @nodegin's 'workaround' which is to use the maintanence-mode-emminent aws-sdk npm pkg...