Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to upload to AWS S3 directly from browser using a pre-signed URL instead of credentials?

We'd like to use Javascript AWS SDK to upload files to S3, but without using credentials at all. Uploading using credentials works, but we cannot generate an AWS IAM user for each of our app users (or should we?)

Therefore, similar to using GET, we'd like the server to generate a pre-signed URL, send it to browser, and have the browser upload to that URL.

However, there are no examples on how to accomplish this. Also, if not setting a credential, even before making the upload to S3 request, the SDK errors with

code: "CredentialsError"
message: "No credentials to load"

The JS SDK docs mention this, so it seems it would be possible:

Pre-signing a putObject (asynchronously)
var params = {Bucket: 'bucket', Key: 'key'};
    s3.getSignedUrl('putObject', params, function (err, url) {
      console.log('The URL is', url);
});
like image 364
user2528332 Avatar asked Mar 20 '14 11:03

user2528332


People also ask

How do I upload a pre-signed URL?

Generating a presigned URL for uploading objects You can use the AWS SDK to generate a presigned URL that you or anyone that you give the URL to can use to upload an object to Amazon S3. When you use the URL to upload an object, Amazon S3 creates the object in the specified bucket.

What is pre-signed URL in AWS S3?

A pre-signed URL allows you to grant temporary access to users who don't have permission to directly run AWS operations in your account. A pre-signed URL is signed with your credentials and can be used by any user.

Can I access S3 bucket from browser?

In short, if you set x-amz-acl: public-read on a file then you can access it as https://s3.amazonaws.com/bucket-name/path-to-file . No need for enabling website hosting, unless you want the pretty hostname and support for index and error documents.


3 Answers

Quiet the old question but it did help me a bit to get it finally done. My solution is based on PHP and JavaScript with jQuery.

I have the entire solution nicely wrapped at https://github.com/JoernBerkefeld/s3SignedUpload but here are the essentials:

api.php:

<?php
require_once '/server/path/to/aws-autoloader.php';
use Aws\Common\Aws;

$BUCKET = "my-bucket";
$CONFIG = "path-to-iam-credentials-file-relative-to-root.php"

function getSignedUrl($filename, $mime) {
    $S3 = Aws::factory( $CONFIG )->get('S3');
    if(!$filename) {
        return $this->error('filename missing');
    }
    if(!$mime) {
        return $this->error('mime-type missing');
    }
    $final_filename = $this->get_file_name($filename);
    try {
        $signedUrl = $S3->getCommand('PutObject', array(
            'Bucket' => $BUCKET,
            'Key' => $this->folder . $final_filename,
            'ContentType' => $mime,
            'Body'        => '',
            'ContentMD5'  => false
        ))->createPresignedUrl('+30 minutes');
    } catch (S3Exception $e) {
        echo $e->getMessage() . "\n";
    }
    $signedUrl .= '&Content-Type='.urlencode($mime);
    return $signedUrl;
}


echo getSignedUrl($_GET['filename'],$_GET['mimetype']);

please make sure to add user authentication to your api.php. Else everyone who knows the path to that file could upload files to your bucket.

credentials.inc.php:

<?php
return array(
    'includes' => array('_aws'),
    'services' => array(
        'default_settings' => array(
            'params' => array(
                'key'    => 'MY-ACCESS-KEY',
                'secret' => 'MY-SECRECT',
                'region'  => 'eu-west-1' // set to your region
            )
        )
    )
);

client.js:

$("input[type=file]").onchange = function () {
    for (var file, i = 0; i < this.files.length; i++) {
        file = this.files[i];
        $.ajax({
            url : s3presignedApiUri,
            data: 'file='+ file.name + '&mime=' + file.type,
            type : "GET",
            dataType : "json",
            cache : false,
        })
        .done(function(s3presignedUrl) {
            $.ajax({
                url : s3presignedUrl,
                type : "PUT",
                data : file,
                dataType : "text",
                cache : false,
                contentType : file.type,
                processData : false
            })
            .done(function(){
                console.info('YEAH', s3presignedUrl.split('?')[0].substr(6));
            }
            .fail(function(){
                console.error('damn...');
            }
        })
    }
};

s3 cors settings (PUT & OPTIONS are actually needed, but cannot enable OPTIONS directly...):

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>HEAD</AllowedMethod>
        <AllowedMethod>DELETE</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>
like image 95
Jörn Berkefeld Avatar answered Oct 16 '22 08:10

Jörn Berkefeld


If you're not using jQuery, this is the minimal you need on the front end:

var xhr = new XMLHttpRequest();
xhr.open('PUT', signedUrl, true);
xhr.setRequestHeader('Content-Type', signedUrlContentType);
xhr.onload = () => {
  if (xhr.status === 200) {
    // success!
  }
};
xhr.onerror = () => {
  // error...
};
xhr.send(file); // `file` is a File object here 

See File object docs: https://developer.mozilla.org/en-US/docs/Web/API/File

Then you can add your upload progress as usual:

xhr.upload.onprogress = (event) => {
  if (event.lengthComputable) {
    var percent = Math.round((event.loaded / event.total) * 100)
    console.log(percent);
  }
};
like image 26
Scott Jungwirth Avatar answered Oct 16 '22 09:10

Scott Jungwirth


In project, on what I am working right now I have file uploads from client directly to S3, in my case it works in few steps:

  1. request pre-signed form with settings for upload, from server (it is signed on a server, because I can not pass access keys to the client, and also I need to apply some limitations to upload)
  2. upload file to S3 using XHR2 (for old browsers you can use hack with hidden iframe or browser plugins like flash)

There is main code parts from it: https://gist.github.com/zxbodya/3cdabd9172bcc89f8ac5

like image 6
Bogdan Savluk Avatar answered Oct 16 '22 08:10

Bogdan Savluk