How to Upload to S3 Using PHP (AWS Signature v4)

You can upload to a S3 bucket with PHP using AWS S3 API. This is tutorial on uploading with S3 API using the new 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.

1) Defining a Few Variables

  1. Add 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.

2) Calculating the Signature and Authorization Header

With AWS Signature version 4, calculating signature becomes even more tougher !

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

3) Finally Send a Curl Request

Here's the complete code:

<?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'); ?>
I'm available for freelancing
If you've liked this article, you'll probably like my code too.

From short code snippets to complete web applications, I code in Javascript & PHP
Low Prices from $20, Fast Delivery Time
I'm available for freelancing
If you've liked this article, you'll probably like my code too.

From short code snippets to complete web applications, I code in Javascript & PHP
Low Prices from $20, Fast Delivery Time