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

amazon s3amazon-web-servicesjavascript

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

Best Answer

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>
Related Topic