0

I keep getting a 403 when trying to upload from the client side. Is this due to not having conditions on the bucket? If I just specify the key - with no accesskey, signature, or policy - it will upload fine.

Bucket policy:

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

CORS (open due to being local development)

<?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>
        <AllowedMethod>DELETE</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

Signature generation

    ///whats returned - in controller

    string_to_sign
    set_s3_direct_post(photo)
    render :json => {
        :policy => @policy,
        :signature => sig,
        :key => Rails.application.secrets.aws_access_key_id,
        :success=>true,
        :store=> photo.photo.store_dir,
        :time => @time_policy,
        :time_date => @date_stamp,
        :form_data => @s3_direct_post
    }

 ------------------------------------------------------------------
    private

   def string_to_sign
     @time = Time.now.utc
     @time_policy = @time.strftime('%Y%m%dT000000Z')
     @date_stamp = @time.strftime('%Y%m%d')

     ret = {"expiration" => 1.day.from_now.utc.xmlschema,
            "conditions" =>  [
                {"bucket" => Rails.application.secrets.aws_bucket},
                {"x-amz-credential": "#{Rails.application.secrets.aws_access_key_id}/#{@date_stamp}/us-west-2/s3/aws4_request"},
                {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
                {"x-amz-date": @time_policy },
            ]
            }

      @policy = Base64.encode64(ret.to_json).gsub(/\n/,'').gsub(/\r/,'')

    end

    def getSignatureKey
        kDate = OpenSSL::HMAC.digest('sha256', ("AWS4" +  Rails.application.secrets.aws_secret_access_key), @date_stamp)
        kRegion = OpenSSL::HMAC.digest('sha256', kDate, 'us-west-2')
        kService = OpenSSL::HMAC.digest('sha256', kRegion, 's3')
        kSigning = OpenSSL::HMAC.digest('sha256', kService, "aws4_request")
    end

    def sig
       sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), getSignatureKey, @policy).gsub(/\n|\r/, '')
    end

Client:

    var self=this;
    $(`#song-upload`).fileupload({
        url: `https://${self._backend.BUCKET}.s3.amazonaws.com`,
        dataType: 'json',
        add: function (e, data) {
          var data_add = data;
          $.ajax({
            url: `${self._backend.SERVER_URL}/api/photo/new`,
            data: {'authorization': `Bearer ${self._auth.isLoggedIn.getCookie('_auth')}`, post_type: 1, file_name:this.file_name},
            type: 'POST',
            success: function(data) {
              if(data.success){
                console.log(data);
                self.key = data.key;
                self.policy = data.policy;
                self.signature = data.signature;
                self.store_dir = data.store;
                self.upload_time = data.time;
                self.upload_date = data.time_date;
                data_add.submit();
               }
            }
          });
        },
        submit: function (e, data) {
          data.formData = {key:`${self.store_dir}/${self.file_name}`,AWSAccessKeyId: self.key, "Policy":self.policy, "x-amz-algorithm":"AWS4-HMAC-SHA256","Signature":self.signature,"x-amz-credential":`${self.key}/${self.upload_date}/us-west-2/s3/aws4_request`, "x-amz-date":self.upload_time};
        },
        progress: function (e, data) {
          var progress = Math.floor(((parseInt(data.loaded)*0.9)  / (parseInt(data.total))) * 100);
          $('#inner-progress').css({'transform':`translateX(${progress}%)`});
          $('#progress-text').text(progress);
        },
        done: function (e, data) {
            $('#inner-progress').css({'transform':`translateX(100%)`});
            $('#progress-text').text(100);
            if(e) console.log(e);
        }
    });
Jack Rothrock
  • 407
  • 1
  • 8
  • 21
  • I did this in past. I was following this article from Heroku. https://devcenter.heroku.com/articles/direct-to-s3-image-uploads-in-rails You can also refer http://stackoverflow.com/a/34830257/3962760 answer for the same. – Nishant Upadhyay Mar 04 '17 at 06:33
  • Just peaked the doc. It looks really good. I'll go through it in the morning. Thanks. Also, do you know why s3 allows me to upload a file with no auth? Is it a missing policy condition? – Jack Rothrock Mar 04 '17 at 07:01
  • Essential things are CORS settings and your S3 bucket object with correct credentials. If these 2 things are proper then you can get your pre-signed URL easily. And once you have that then no other auth require. – Nishant Upadhyay Mar 04 '17 at 07:05
  • In case the bucket policy is wrong, take a look at the example policies in the aws docs to verify. http://docs.aws.amazon.com/de_de/IAM/latest/UserGuide/access_policies_examples.html – Linus Mar 05 '17 at 21:25

1 Answers1

0

If someone has this, and is trying to do a javascript upload, try plugging in the values into the html file found here. Amazon will tell you the actual errors, instead of just a 403 response.

I was missing the ["starts-with", "$key", "uploads"] in my base64'd config.

Here's my end configurations:

Bucket Config:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Allow Get",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::example-development/*"
        },
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789:user/example"
            },
            "Action": "s3:*",
            "Resource": ["arn:aws:s3:::example-development/*","arn:aws:s3:::example-development"]
        }
    ]
}

Bucket

<?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>
        <AllowedMethod>DELETE</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

Backend:

       string_to_sign
       set_s3_direct_post(song)
       render :json => {
                :policy => @policy,
                :signature => sig,
                :key => Rails.application.secrets.aws_access_key_id,
                :success=>true,
                :store=> song.song.store_dir,
                :time => @time_policy,
                :time_date => @date_stamp,
                :form_data => @s3_direct_post
       }

def string_to_sign

    @time = Time.now.utc
    @time_policy = @time.strftime('%Y%m%dT000000Z')
    @date_stamp = @time.strftime('%Y%m%d')

     ret = {"expiration" => 10.hours.from_now.utc.iso8601,
            "conditions" =>  [
                {"bucket" => 'waydope-development'},
                {"x-amz-credential": "#{Rails.application.secrets.aws_access_key_id}/#{@date_stamp}/us-west-2/s3/aws4_request"},
                {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
                {"x-amz-date": @time_policy },
                ["starts-with", "$key", "uploads"]
            ]
            }
    @policy = Base64.encode64(ret.to_json).gsub(/\n|\r/, '')

end

def getSignatureKey
        kDate = OpenSSL::HMAC.digest('sha256', ("AWS4" +  Rails.application.secrets.aws_secret_access_key), @date_stamp)
        kRegion = OpenSSL::HMAC.digest('sha256', kDate, 'us-west-2')
        kService = OpenSSL::HMAC.digest('sha256', kRegion, 's3')
        kSigning = OpenSSL::HMAC.digest('sha256', kService, "aws4_request")
    end

def sig
        # sig = Base64.encode64(OpenSSL::HMAC.digest('sha256', getSignatureKey,  @policy)).gsub(/\n|\r/, '')
        sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), getSignatureKey, @policy).gsub(/\n|\r/, '')
end

Client:

var self=this;
    $(`#song-upload`).fileupload({
        url: `https://${self._backend.BUCKET}.s3.amazonaws.com`,
        dataType: 'multipart/form-data',
        add: function (e, data) {
          var data_add = data;
          $.ajax({
            url: `${self._backend.SERVER_URL}/api/music/new`,
            data: {'authorization': `Bearer ${self._auth.isLoggedIn.getCookie('_waydope')}`, post_type: 1, file_name:this.file_name},
            type: 'POST',
            success: function(data) {
              if(data.success){
                console.log(data);
                self.key = data.key;
                self.policy = data.policy;
                self.signature = data.signature;
                self.store_dir = data.store;
                self.upload_time = data.time;
                self.upload_date = data.time_date;
                data_add.submit();
               }
            }
          });
        },
        submit: function (e, data) {
          data.formData = {key:`${self.store_dir}/${self.file_name}`, "Policy":self.policy,"X-Amz-Signature":self.signature,"X-Amz-Credential":`${self.key}/${self.upload_date}/us-west-2/s3/aws4_request`,"X-Amz-Algorithm":"AWS4-HMAC-SHA256", "X-Amz-Date":self.upload_time, "acl": "public-read"};
        },
        progress: function (e, data) {
          var progress = Math.floor(((parseInt(data.loaded)*0.9)  / (parseInt(data.total))) * 100);
          $('#inner-progress').css({'transform':`translateX(${progress}%)`});
          $('#progress-text').text(progress);
        },
        done: function (e, data) {
            $('#inner-progress').css({'transform':`translateX(100%)`});
            $('#progress-text').text(100);
            if(e) console.log(e);
        }
    });
Jack Rothrock
  • 407
  • 1
  • 8
  • 21
  • Just noting that you don't need to generate all those AWS S3 request parameters by hand, you can just use the official `aws-sdk` gem to generate them for you: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_post-instance_method – Janko Mar 11 '17 at 00:43
  • I may have just been using it wrong (or entered things incorrectly), but from what I saw, the credentials and policy aren't returned from the gem. Heroku has a [good guide on using the gem with browser uploads](https://devcenter.heroku.com/articles/direct-to-s3-image-uploads-in-rails) – Jack Rothrock Mar 11 '17 at 19:51
  • 1
    I think so, because on `aws-sdk` version 2.7.11 the following snippet for me returns both `policy` and `x-amz-credential` parameters: `Aws::S3::Resource.new(access_key_id: "abc", secret_access_key: "xyz", region: "eu-west-1").bucket("bucket").object("foo").presigned_post.fields` – Janko Mar 12 '17 at 03:53