Skip to content

Instantly share code, notes, and snippets.

@nmcgann
Last active October 9, 2015 06:03
Show Gist options
  • Save nmcgann/caaaca7a992fe7997ae3 to your computer and use it in GitHub Desktop.
Save nmcgann/caaaca7a992fe7997ae3 to your computer and use it in GitHub Desktop.
Amazon S3 Version 4 API request signer script
<?php
//TEST CODE
//Simple example of links to files in a privately accessible s3 bucket being authorised.
//If the link is followed directly a 403 error will be returned with an xml error page.
//Instead jquery catches the click and creates an authorisation for the request assisted by
//a server that holds the access keys. The region, bucket and key are extracted and then a
//server-side authorisation query string is requested, appended to the link href and then the
//file is requested from s3.
// Bucket needs CORS turning on (for GET) and s3:GetObject permission needs to be present.
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=utf-8>
</head>
<body>
<div id="s3-files">
<a href="https://s3-eu-west-1.amazonaws.com/my-bucket/my-first-file.jpg">my-first-file.jpg</a><br />
<a href="https://s3-eu-west-1.amazonaws.com/my-bucket/my-second-file.jpg">my-second-file.jpg</a>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.js"></script>
<script>
$(document).ready(function(){
$('#s3-files').on('click', 'a', function(e){
e.preventDefault();
//grab url, extract region, bucket and key
var href = $(this).attr('href'),
matches = href.match(/^.*\/\/s3-(.*?)\.amazonaws.com\/(.*?)\/(.*)$/);
//matches[1]=region, matches[2]=bucket, matches[3]=key
if(matches && matches.length === 4){
//sign request with access_key and secret_key server side
$.get('s3-signer.php',
{type: 'GET',
bucket: matches[2],
key: matches[3],
region: matches[1]
},'json').done(function(data){
var url = data.url + '?' + data.qstring;
//fetch the link with the appended authorisation
window.location.href = url;
});
}
});
});
</script>
</body>
</html>
<?php
/*
* s3 V4 API server-side request signer component for client-side javascript to use.
*
* Implements a generalised request signer for the query string auth version of the AWS V4 API
* http://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html
* http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
*
* Access amd secret keys are either set here or in environment variables - implementation dependent.
* Called by javascript client side (GET) with the following parameters:
* type - request verb GET/PUT/DELETE/HEAD etc.
* key - key name.
* expiry - X-Amz-Expires header (300sec default)
* bucket - bucket name.
* region - aws region.
* extra - json array of additional headers to be added
* e.g. as part of a PUT request[{"Content-Length":"879568"},{"X-Amz-Storage-Class":"REDUCED_REDUNDANCY"}]
*
* Returns a json object with the target url, final query string and key name.
* url, qstring, key.
* The complete request is just url+"?"+qstring. (plus the body to be attached).
*
* Neil McGann. 8th Oct 2015
*
* The MIT License (MIT)
*
* Copyright (c) 2015 Neil McGann
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
define('TEST_MODE', false); //prints to screen instead of json for test purposes
//keys
$access_key = 'MY-ACCESS-KEY';
$secret_key = 'MY-SECRET-KEY';
//defaults
$bucket = 'MY-DEFAULT-BUCKET'; //default bucket name 'my-bucket'
$region = 'MY-DEFAULT-REGION'; //default region e.g. 'eu-west-1'
$expiry = 300; //expiry in seconds for X-Amz-Expires header
//dates
$short_date = gmdate('Ymd'); //short date
$iso_date = gmdate("Ymd\THis\Z"); //iso format date
//$expiration_date = gmdate('Y-m-d\TG:i:s\Z', strtotime('+5 minutes'));
// -------------------------------------------------------------------------- //
$requestType = isset($_GET['type']) ? trim($_GET['type']) : 'GET';
$key = isset($_GET['key']) ? trim($_GET['key']) : '';
$expiry = isset($_GET['expiry']) ? (int) $_GET['expiry'] : $expiry;
$bucket = isset($_GET['bucket']) ? trim($_GET['bucket']) : $bucket;
$region = isset($_GET['region']) ? trim($_GET['region']) : $region;
//see if a json array of extra headers has been passed as a parameter
$extra = [];
if(isset($_GET['extra'])) {
$extraArray = @json_decode($_GET['extra'], true);
//if duff json - ignore
$extra = ($extraArray !== null) ? $extraArray : $extra;
}
$uri = rawurlencode($key); //needs to be raw encode version (before qsp)
$credential = $access_key . "/" . $short_date . "/" . $region . "/s3/aws4_request";
$headers = [];
$headers[] = ["X-Amz-Algorithm" => "AWS4-HMAC-SHA256"];
$headers[] = ["X-Amz-Credential" => $credential];
$headers[] = ["X-Amz-Date" => $iso_date];
$headers[] = ["X-Amz-Expires" => $expiry];
$headers[] = ["X-Amz-SignedHeaders" => "host"];
$headers = array_merge($headers, $extra);
//sort headers into alphabetical order - must be sorted
usort($headers, function($a, $b){
return strcmp(key($a), key($b));
});
$queryString = "";
foreach($headers as $header){
$queryString .= urlencode(key($header)) . "=" . urlencode(current($header)) . "&";
}
$queryString = substr($queryString, 0, -1); //chop trailing &
$canonicalRequest =
"$requestType\n" . //verb
"/" . $bucket . "/" . $uri . "\n" .
$queryString . "\n" . //canonical query string
"host:" . "s3-" . $region . ".amazonaws.com\n" . //canonical headers
"\n" . //canonical headers
"host\n" . //signed headers
"UNSIGNED-PAYLOAD";
$stringToSign =
"AWS4-HMAC-SHA256\n" .
$iso_date . "\n" .
$short_date . "/" . $region . "/s3/aws4_request" . "\n" .
hash("sha256", $canonicalRequest, false);
$kDate = hash_hmac('sha256', $short_date, 'AWS4' . $secret_key, true);
$kRegion = hash_hmac('sha256', $region, $kDate, true);
$kService = hash_hmac('sha256', "s3", $kRegion, true);
$kSigning = hash_hmac('sha256', "aws4_request", $kService, true);
$signature = hash_hmac('sha256', $stringToSign, $kSigning);
$url = "https://s3-" . $region . ".amazonaws.com/" . $bucket . "/" . $uri;
$qsp = $queryString . "&X-Amz-Signature=" . $signature;
//return
$response = ['url' => $url, 'qstring' => $qsp, 'key' => $key];
//Test code to show params then complete signed url in a text box - can copy &
//test with Postman on Chrome or similar. Set define to false when not required
if(defined('TEST_MODE') && TEST_MODE == true){
echo "<pre>";
echo "\n##Params:\n";
echo "access key: " . $access_key ."\n";
echo "secret key: " . $secret_key ."\n";
echo "type: " . $requestType ."\n";
echo "key: " . $key ."\n";
echo "bucket: " . $bucket ."\n";
echo "region: " . $region ."\n";
echo "expiry: " . $expiry ."\n";
echo "extra: ";
print_r($extra);
echo "\n";
echo "\n##Signed Url:\n";
echo "<textarea rows=\"6\" cols=\"80\">" . $url . '?' . $qsp . "</textarea>";
echo "\n\n";
}else{
header('Content-Type: application/json');
exit(json_encode($response));
}
/* end */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment