Search code examples
rubyamazon-s3carrierwavejquery-file-uploadcarrierwave-direct

Upload file to s3 on client side with rails, carrierwave-direct, and jquery file upload


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);
        }
    });

Solution

  • 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);
            }
        });