'AWS S3 Generating Signed Urls ''AccessDenied''

I am using NodeJs to upload files to AWS S3. I want the client to be able to download the files securely. So I am trying to generate signed URLs, that expire after one usage. My code looks like this:

Uploading

const s3bucket = new AWS.S3({
    accessKeyId: 'my-access-key-id',
    secretAccessKey: 'my-secret-access-key',
    Bucket: 'my-bucket-name',
})
const uploadParams = {
    Body: file.data,
    Bucket: 'my-bucket-name',
    ContentType: file.mimetype,
    Key: `files/${file.name}`,
}
s3bucket.upload(uploadParams, function (err, data) {
    // ...
})

Downloading

const url = s3bucket.getSignedUrl('getObject', {
    Bucket: 'my-bucket-name',
    Key: 'file-key',
    Expires: 300,
})

Issue

When opening the URL I get the following:

This XML file does not appear to have any style information associated with it. The document tree is shown below.
<Error>
    <Code>AccessDenied</Code>
    <Message>
        There were headers present in the request which were not signed
    </Message>
    <HeadersNotSigned>host</HeadersNotSigned>
    <RequestId>D63C8ED4CD8F4E5F</RequestId>
    <HostId>
        9M0r2M3XkRU0JLn7cv5QN3S34G8mYZEy/v16c6JFRZSzDBa2UXaMLkHoyuN7YIt/LCPNnpQLmF4=
    </HostId>
</Error>

I coultn't manage to find the mistake. I would really appreciate any help :)



Solution 1:[1]

Highest upvoted answer here technically works but isn't practical since it's opening up the bucket to be public.

I had the same problem and it was due to the role that was used to generate the signed url. The role I was using had this:

- Effect: Allow
  Action: 
    - "s3:ListObjects"
    - "s3:GetObject"
    - "s3:GetObjectVersion"
    - "s3:PutObject"
  Resource:
    - "arn:aws:s3:::(bucket-name-here)"

But the bucket name alone wasn't enough, I had to add a wildcard on the end to designate access to whole bucket:

- Effect: Allow
  Action: 
    - "s3:ListObjects"
    - "s3:GetObject"
    - "s3:GetObjectVersion"
    - "s3:PutObject"
  Resource:
    - "arn:aws:s3:::(bucket-name-here)/*"

Solution 2:[2]

I battled with this as well with an application using Serverless Framework.

My fix was adding S3 permissions to the IAM Role inside of the serverless.yml file.

I'm not exactly sure how s3 makes the presigned URL but it turns out they take your IAM role into account.

Adding all s3 actions did the trick. This is what the IAM role looks like for S3 ?

iamRoleStatements:
  - Effect: Allow
      Action:
        - 's3:*'
      Resource:
        - 'arn:aws:s3:::${self:custom.imageBucket}/*'

Solution 3:[3]

Your code looks good but I think you are missing the signatureVersion: 'v4' parameter while creating the s3bucket object. Please try the below updated code.

const s3bucket = new AWS.S3({
    signatureVersion: 'v4',
    accessKeyId: 'my-access-key-id',
    secretAccessKey: 'my-secret-access-key',
    Bucket: 'my-bucket-name',
})
const uploadParams = {
    Body: file.data,
    Bucket: 'my-bucket-name',
    ContentType: file.mimetype,
    Key: `files/${file.name}`,
}
s3bucket.upload(uploadParams, function (err, data) {
    // ...
})
const url = s3bucket.getSignedUrl('getObject', {
    Bucket: 'my-bucket-name',
    Key: 'file-key',
    Expires: 300,
})

For more about signatureVersion: 'v4' see the below links

https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html

https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html

You can also try out the below nodejs library that create presigned url

https://www.npmjs.com/package/aws-signature-v4

Solution 4:[4]

I kept having a similar problem but mine were due to region settings. In our back end we had some configuration settings for the app.

One of which was "region": "us-west-2" so the presigned url was created with this region but when it was called on the front end the region was set to "us-west-1".

Changing it to be the same fixed the issue.

Solution 5:[5]

If your s3 files are encrypted than make sure that your policy also access to encryption key and related actions.

Solution 6:[6]

I had the same issue when i'm locally testing my lambda function its works but after deploy it didn't work. once i add the s3 full access to lambda function it worked.

Solution 7:[7]

I saw this problem recently when moving from a bucket that was created a while ago to one created recently.

It appears that v2 pre-signed links (for now) continue to work against older buckets while new buckets are mandated to use v4.

Revised Plan – Any new buckets created after June 24, 2020 will not support SigV2 signed requests, although existing buckets will continue to support SigV2 while we work with customers to move off this older request signing method.

Even though you can continue to use SigV2 on existing buckets, and in the subset of AWS regions that support SigV2, I encourage you to migrate to SigV4, gaining some important security and efficiency benefits in the process.

https://docs.amazonaws.cn/AmazonS3/latest/API/sigv4-query-string-auth.html#query-string-auth-v4-signing-example

Our solution involved updating the AWS SDK to use this by default; I suspect newer versions probably already default this setting.

https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-other.html#config-setting-aws-s3-usesignatureversion4

Solution 8:[8]

After banging my head for many hours with this same issue. I noticed that my account had a MFA setup , making the generation of the signed url with only the accessKeyId and secretAccesKey useless.

The solution was installing this https://github.com/broamski/aws-mfa

After running it , it asks to create a .aws/credentials file, where you must input your access id / secret and aws_mfa_device . The later will look something like

aws_mfa_device = arn:aws:iam::youruserid:mfa/youruser

The data can be found in your user in the aws console (Website)

After that you will find that credentials are populated with new keys with 1 week duration iirc.

Then simply generate a url again

AWS.config.update({ region: 'xxx' });
var s3 = new AWS.S3();

var presignedGETURL = s3.getSignedUrl('putObject', {
    Bucket: 'xxx',
    Key: 'xxx', //filename
    Expires: xxx, //time to expire in seconds,
    ContentType: 'xxx'
});

And this time it will work.

Remember to NOT pass any credentials to AWS.config , since they will be automatically picked from the .aws/credentials folder.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Spencer Sutton
Solution 2 DylanA
Solution 3 Vaisakh PS
Solution 4 Ju66ernaut
Solution 5 Arun
Solution 6 ashen madusanka
Solution 7 cwash
Solution 8 mouchin777