Ex_aws s3 file upload return error of SignatureDoesNotMatch

mix.exs

defps deps do
  [
     ...
     {:ex_aws, "~> 2.0"},
     {:ex_aws_s3, "~> 2.0"},
     {:hackney, "~> 1.9"},
     {:sweet_xml, "~> 0.6"},
     {:uuid, "~> 1.1"}
  ]
end

config.exs

config :ex_aws,
  debug_request: true,
  access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role],
  secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role],
  s3: [
    scheme: "https://",
    host: "s3.ap-southeast-1.amazonaws.com",
    region: "ap-southeast-1"
  ]

How I generate presign url

def get_presign_url(file_type) do
  uuid = UUID.uuid4 |> UUID.uuid5(random_string(68))
  bucket = "bucketname-#{Mix.env}"
  config = %{region: "ap-southeast-1"}
  file_ext = grep_file_type(file_type)
  query_params = [{"ContentType", file_type}, {"ACL", "public-read"}]
  presign_options =[virtual_host: false, query_params: query_params]
  
  {:ok, url} = ExAws.Config.new(:s3, config)
  |> ExAws.S3.presigned_url(:put, bucket, "#{uuid}.#{file_ext}", presign_options)

  %{upload_url: url, url: get_image_url(bucket, uuid, file_ext)}
end

defp grep_file_type("image/" <> type), do: type
defp grep_file_type("audio/" <> type), do: type

defp get_image_url(bucket, uuid, ext), do: "https://s3.amazonaws.com/#{bucket}/#{uuid}.#{ext}"

list object from my bucket

iex> ExAws.S3.list_objects("bucketname-dev") |> ExAws.request()
:ok,
 %{
 body: %{
   common_prefixes: [],
   contents: [],
   is_truncated: "false",
   marker: "",
   max_keys: "1000",
   name: "bucketname-dev",
   next_marker: "",
   prefix: ""
 },
 headers: [
   {"x-amz-id-2", "+3B7XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX="},
   {"x-amz-request-id", "BDXXXXXXXXXXXXXXXXXXXX5"},
   {"Date", "Fri, 06 Jul 2018 06:42:08 GMT"},
   {"x-amz-bucket-region", "ap-southeast-1"},
   {"Content-Type", "application/xml"},
   {"Transfer-Encoding", "chunked"},
   {"Server", "AmazonS3"}
 ],
 status_code: 200
}}

inside postman

// presignUrl
POST http://localhost:4000/graphql
HEADERS Content-Type text/plain
BODY:
   {
      presignUrl(fileType: "image/png") {
        url
        uploadUrl
      }
   }
// uploadUrl return https://s3.ap-southeast-1.amazonaws.com/bucketname-dev/xxxxx-uuid.png/...

// uploadFile
POST https://s3.ap-southeast-1.amazonaws.com/bucketname-dev/xxxx-uuid.png/...
BODY:
  // there is this option to choose binary file
  // click send and this is what it returned

<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
<AWSAccessKeyId>XXXXXXXXXXXXXXXXXX</AWSAccessKeyId>
<StringToSign>AWS4-HMAC-SHA256
20180706T064038Z
20180706/ap-southeast-1/s3/aws4_request XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</StringToSign><SignatureProvided>3d4d1e5cd41e758d7673e2e34fddd2c8879450d1b1ff6c86371d03703c159dd6</SignatureProvided>
<StringToSignBytes>41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 31 38 30 37 30 36 54 30 36 34 30 33 38 5a 0a 32 30 31 38 30 37 30 36 2f 61 70 2d 73 6f 75 74 68 65 61 73 74 2d 31 2f 73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 64 39 65 38 35 35 65 38 36 37 61 65 63 32 38 37 32 66 65 34 30 61 30 64 64 65 65 61 31 32 63 33 30 37 36 62 33 64 36 32 33 36 30 65 62 32 61 39 62 63 65 63 61 61 34 37 32 62 36 39 39 39 66 61</StringToSignBytes>
<CanonicalRequest>POST
/bucketname-dev/fc8a24c2-dc5d-5769-8dcb-aea847ed40d4.png
ACL=public-read&amp;ContentType=image%2Fpng&amp;X-Amz-Algorithm=AWS4- 
HMAC-SHA256&amp;X-Amz- 
Credential=XXXXXXXXXXXXXXXXXXXXXXXXX%2XXXXXXXXXXX%2Fap-southeast- 
1%2Fs3%2Faws4_request&amp;X-Amz-Date=20180706T064038Z&amp;X-Amz- 
Expires=3600&amp;X-Amz-SignedHeaders=acl%3Bcontenttype%3Bhost
acl:
contenttype:
host:s3.ap-southeast-1.amazonaws.com

acl;contenttype;host
UNSIGNED-PAYLOAD</CanonicalRequest>
<CanonicalRequestBytes>50 4f 53 54 0a 2f 6b 6f 6f 6d 70 69 2d 6c 65 61 72 6e 2d 73 65 72 76 65 72 2d 64 65 76 2f 66 63 38 61 32 34 63 32 2d 64 63 35 64 2d 35 37 36 39 2d 38 64 63 62 2d 61 65 61 38 34 37 65 64 34 30 64 34 2e 70 6e 67 0a 41 43 4c 3d 70 75 62 6c 69 63 2d 72 65 61 64 26 43 6f 6e 74 65 6e 74 54 79 70 65 3d 69 6d 61 67 65 25 32 46 70 6e 67 26 58 2d 41 6d 7a 2d 41 6c 67 6f 72 69 74 68 6d 3d 41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 26 58 2d 41 6d 7a 2d 43 72 65 64 65 6e 74 69 61 6c 3d 41 4b 49 41 49 47 41 4c 44 55 53 32 53 42 4b 4f 57 36 58 51 25 32 46 32 30 31 38 30 37 30 36 25 32 46 61 70 2d 73 6f 75 74 68 65 61 73 74 2d 31 25 32 46 73 33 25 32 46 61 77 73 34 5f 72 65 71 75 65 73 74 26 58 2d 41 6d 7a 2d 44 61 74 65 3d 32 30 31 38 30 37 30 36 54 30 36 34 30 33 38 5a 26 58 2d 41 6d 7a 2d 45 78 70 69 72 65 73 3d 33 36 30 30 26 58 2d 41 6d 7a 2d 53 69 67 6e 65 64 48 65 61 64 65 72 73 3d 61 63 6c 25 33 42 63 6f 6e 74 65 6e 74 74 79 70 65 25 33 42 68 6f 73 74 0a 61 63 6c 3a 0a 63 6f 6e 74 65 6e 74 74 79 70 65 3a 0a 68 6f 73 74 3a 73 33 2e 61 70 2d 73 6f 75 74 68 65 61 73 74 2d 31 2e 61 6d 61 7a 6f 6e 61 77 73 2e 63 6f 6d 0a 0a 61 63 6c 3b 63 6f 6e 74 65 6e 74 74 79 70 65 3b 68 6f 73 74 0a 55 4e 53 49 47 4e 45 44 2d 50 41 59 4c 4f 41 44</CanonicalRequestBytes>
<RequestId>XXXXXXXXXXXXX</RequestId>
<HostId>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=</HostId>

on AWS S3 bucket config, Access control list, everyone has permission to list and write objects,

Bucket policy

{
"Version": "2012-10-17",
"Statement": [
    {
        "Sid": "AddPerm",
        "Effect": "Allow",
        "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::bucketname-dev/*"
        }
    ]
}

Bucket CORS

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>

How I run the server

// .env
export AWS_ACCESS_KEY_ID=XXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXX

// in shell a process
source .env
mix phx.server

I have my another project which is self learn and I am able to get the file upload to S3 working, but not this next project, also when I return to the previous project that has file upload to S3 working in order to test for the s3 upload, it does not work also and give me the same error

I could successfully upload file once I removed the query_params from the presign_options

def get_presign_url(file_type) do
  ...
  # query_params = [{"ContentType", file_type}, {"ACL", "public-read"}]
  query_params = []
  presign_options = [virtual_host: false, query_params: query_params]
  ...
end

Sorry for being off-topic, but Mix.env won’t be available in get_presign_url/1 for the prod environment if you use releases.

In the above postman request, the canonical request is supposed to follow the form of Lowercase(HeaderName) + ':' + Trimall(HeaderValue) + '\n' (from Create a signed AWS API request - AWS Identity and Access Management), so this would cause a signature issue

So it should look more like:

1 Like

Oh Yes I see, Thank you very much ^.^

I see man, It is working now thanks you very much. So i change it back to my old code like this:

def get_presign_url(file_type) do
  ...
  query_params = [{"ContentType", file_type}, {"ACL", "public-read"}]
  #query_params = []
  presign_options = [virtual_host: false, query_params: query_params]
  ...
end

Here is my postman request header:

PUT ..links../http1.1
Host: bucketname-dev.s3.amazonaws.com
Content-Type: image/png
ACL: public-read
Postman-Token: 68a9b5b7-d75d-4cf8-a6e3-966c3e6c4570

What I did inside the postman was to add the ACL: public-read to it before sending the request :smiley: