How to Upload Files to AWS S3 using API (PHP)

Published on January 18, 2017

Use-Cases of this code snippet

  • Know how to upload files to a AWS S3 bucket using its API.
  • Know how to create API signature & authorization header using AWS Signature v4.

Files can be uploaded to AWS S3 using its API. This tutorial explains how to do so using PHP.

You can upload files to a S3 bucket with PHP using AWS S3 API. This is tutorial on uploading files with S3 API using the AWS Signature v4.

There is an official PHP library provided by AWS. But frankly speaking, there's no real reason to use the 1MB PHP SDK for such a small task. You can write custom code to implement S3 upload using PHP CURL in only 50 lines.

Defining Settings

  1. Define your AWS API keys, bucket, region and S3 host name for that region.

    // AWS API keys
    $aws_access_key_id = 'XXXXXXXXXXXXXXXXX';
    $aws_secret_access_key = 'XXXXXXXXXXXXXXXXX';
    
    // Bucket
    $bucket_name = 'your-s3-bucket-name';
    
    // AWS region and Host Name (Host names are different for each AWS region)
    // As an example these are set to us-east-1 (US Standard)
    $aws_region = 'us-east-1';
    $host_name = $bucket_name . '.s3.amazonaws.com';
    

    The above code is demonstrated for the us-east-1 region. Please note that the host name will be different for each region. For us-west-2 region, the host name will be :

    $aws_region = 'us-west-2';
    $host_name = $bucket_name . '.s3-us-west-2.amazonaws.com';
    

    You can find out the host name for your region here

  2. Add the location of the file in your server which is to be uploaded. Set the permissions for the to-be-uploaded file.
    Also add the desired name of the file on S3 (without spaces). Add the content type.

    // Server path where content is present. This is just an example
    $content_path = 'img/color.png';
    $content = file_get_contents($content_path);
    
    // AWS file permissions
    $content_acl = 'authenticated-read';
    
    // MIME type of file. Very important to set if you later plan to load the file from a S3 url in the browser (images, for example)
    $content_type = 'image/png';
    // Name of content on S3
    $content_title = 'sample-image-8.png';
    

    A very important thing is to ensure that you set the MIME content type of the file. Without this the content type of the file will be set to application/x-www-form-urlencoded (which would look really strange on an JPEG, for example). Setting this option is also very important if you are planning to host this file over an application. For example, browser will only render a JPEG file if its Content-Type is set to image/jpeg, otherwise a force download option will be shown.

Calculating Signature & Authorization Header using AWS Signature v4

AWS Signature version 4 is very particular to details. The code needs to be very precise otherwise it won't work.

  1. Create a canonical request as given in Task 1

    // Service name for S3
    $aws_service_name = 's3';
    
    // UTC timestamp and date
    $timestamp = gmdate('Ymd\THis\Z');
    $date = gmdate('Ymd');
    
    // HTTP request headers as key & value
    $request_headers = array();
    $request_headers['Content-Type'] = $content_type;
    $request_headers['Date'] = $timestamp;
    $request_headers['Host'] = $host_name;
    $request_headers['x-amz-acl'] = $content_acl;
    $request_headers['x-amz-content-sha256'] = hash('sha256', $content);
    // Sort it in ascending order
    ksort($request_headers);
    
    // Canonical headers
    $canonical_headers = [];
    foreach($request_headers as $key => $value) {
    	$canonical_headers[] = strtolower($key) . ":" . $value;
    }
    $canonical_headers = implode("\n", $canonical_headers);
    
    // Signed headers
    $signed_headers = [];
    foreach($request_headers as $key => $value) {
    	$signed_headers[] = strtolower($key);
    }
    $signed_headers = implode(";", $signed_headers);
    
    // Cannonical request 
    $canonical_request = [];
    $canonical_request[] = "PUT";
    $canonical_request[] = "/" . $content_title;
    $canonical_request[] = "";
    $canonical_request[] = $canonical_headers;
    $canonical_request[] = "";
    $canonical_request[] = $signed_headers;
    $canonical_request[] = hash('sha256', $content);
    $canonical_request = implode("\n", $canonical_request);
    $hashed_canonical_request = hash('sha256', $canonical_request);
    

    At a minimum, S3 API requires only 3 headers - Date, Host and x-amz-content-sha256

    As you can see 2 extra headers have been included. x-amz-acl has been included due to the fact that we are passing a permission for the file. If you leave out this header, file will be uploaded with permission as private.

    Content-Type has been included due to reasons discussed in the above section.

  2. Create a string to sign as given in Task 2

    // AWS Scope
    $scope = [];
    $scope[] = $date;
    $scope[] = $aws_region;
    $scope[] = $aws_service_name;
    $scope[] = "aws4_request";
    
    // String to sign
    $string_to_sign = [];
    $string_to_sign[] = "AWS4-HMAC-SHA256"; 
    $string_to_sign[] = $timestamp; 
    $string_to_sign[] = implode('/', $scope);
    $string_to_sign[] = $hashed_canonical_request;
    $string_to_sign = implode("\n", $string_to_sign);
    
  3. Calculate the signature as given in Task 3

    // Signing key
    $kSecret = 'AWS4' . $aws_secret_access_key;
    $kDate = hash_hmac('sha256', $date, $kSecret, true);
    $kRegion = hash_hmac('sha256', $aws_region, $kDate, true);
    $kService = hash_hmac('sha256', $aws_service_name, $kRegion, true);
    $kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
    
    // Signature
    $signature = hash_hmac('sha256', $string_to_sign, $kSigning);
    
  4. Include Authorization header as given in Task 4

    // Authorization
    $authorization = [
    	'Credential=' . $aws_access_key_id . '/' . implode('/', $scope),
    	'SignedHeaders=' . $signed_headers,
    	'Signature=' . $signature,
    ];
    $authorization = 'AWS4-HMAC-SHA256' . ' ' . implode( ',', $authorization);
    
    // Curl headers
    $curl_headers = [ 'Authorization: ' . $authorization ];
    foreach($request_headers as $key => $value) {
    	$curl_headers[] = $key . ": " . $value;
    }
    

Complete Codes

Below are the complete PHP codes to upload a files to a AWS S3 bucket using PHP.

<?php
/// AWS API keys
$aws_access_key_id = 'XXXXXXXXXXXXXXXXX';
$aws_secret_access_key = 'XXXXXXXXXXXXXXXXX';

// Bucket
$bucket_name = 'your-s3-bucket-name';

// AWS region and Host Name (Host names are different for each AWS region)
// As an example these are set to us-east-1 (US Standard)
$aws_region = 'us-east-1';
$host_name = $bucket_name . '.s3.amazonaws.com';

// Server path where content is present. This is just an example
$content_path = 'img/color.png';
$content = file_get_contents($content_path);

// AWS file permissions
$content_acl = 'authenticated-read';

// MIME type of file. Very important to set if you later plan to load the file from a S3 url in the browser (images, for example)
$content_type = 'image/png';
// Name of content on S3
$content_title = 'sample-image-8.png';

// Service name for S3
$aws_service_name = 's3';

// UTC timestamp and date
$timestamp = gmdate('Ymd\THis\Z');
$date = gmdate('Ymd');

// HTTP request headers as key & value
$request_headers = array();
$request_headers['Content-Type'] = $content_type;
$request_headers['Date'] = $timestamp;
$request_headers['Host'] = $host_name;
$request_headers['x-amz-acl'] = $content_acl;
$request_headers['x-amz-content-sha256'] = hash('sha256', $content);
// Sort it in ascending order
ksort($request_headers);

// Canonical headers
$canonical_headers = [];
foreach($request_headers as $key => $value) {
	$canonical_headers[] = strtolower($key) . ":" . $value;
}
$canonical_headers = implode("\n", $canonical_headers);

// Signed headers
$signed_headers = [];
foreach($request_headers as $key => $value) {
	$signed_headers[] = strtolower($key);
}
$signed_headers = implode(";", $signed_headers);

// Cannonical request 
$canonical_request = [];
$canonical_request[] = "PUT";
$canonical_request[] = "/" . $content_title;
$canonical_request[] = "";
$canonical_request[] = $canonical_headers;
$canonical_request[] = "";
$canonical_request[] = $signed_headers;
$canonical_request[] = hash('sha256', $content);
$canonical_request = implode("\n", $canonical_request);
$hashed_canonical_request = hash('sha256', $canonical_request);

// AWS Scope
$scope = [];
$scope[] = $date;
$scope[] = $aws_region;
$scope[] = $aws_service_name;
$scope[] = "aws4_request";

// String to sign
$string_to_sign = [];
$string_to_sign[] = "AWS4-HMAC-SHA256"; 
$string_to_sign[] = $timestamp; 
$string_to_sign[] = implode('/', $scope);
$string_to_sign[] = $hashed_canonical_request;
$string_to_sign = implode("\n", $string_to_sign);

// Signing key
$kSecret = 'AWS4' . $aws_secret_access_key;
$kDate = hash_hmac('sha256', $date, $kSecret, true);
$kRegion = hash_hmac('sha256', $aws_region, $kDate, true);
$kService = hash_hmac('sha256', $aws_service_name, $kRegion, true);
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);

// Signature
$signature = hash_hmac('sha256', $string_to_sign, $kSigning);

// Authorization
$authorization = [
	'Credential=' . $aws_access_key_id . '/' . implode('/', $scope),
	'SignedHeaders=' . $signed_headers,
	'Signature=' . $signature
];
$authorization = 'AWS4-HMAC-SHA256' . ' ' . implode( ',', $authorization);

// Curl headers
$curl_headers = [ 'Authorization: ' . $authorization ];
foreach($request_headers as $key => $value) {
	$curl_headers[] = $key . ": " . $value;
}

$url = 'https://' . $host_name . '/' . $content_title;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($http_code != 200) 
	exit('Error : Failed to upload');

?>
Loading Comments