How to Create Expiring Links for Amazon’s S3 with PHP December 4, 2012
Posted by Tournas Dimitrios in Amazon-aws, PHP.trackback
Amazon’s Simple Storage Service (Amazon S3) is a web-based service for storing and retrieving any amount of data at any time , from anywhere on the web . These tasks (storing / retrieving of data) can be accomplished using various tools . For instance , browser-plugin , web-interface , command-line , programming libraries or a REST API . I recommend the first two tools (browser-plugin , web-interface) as a starting point for those that doesn’t have familiarity with Amazon’s web-storage , but at some point , more sophisticated tools are needed to accomplish customized functionality or automated tasks (via scripts) . This article will present a practical example , how to use the S3-REST-API service to download a specific object (in Amazon’s parlance , an object is any file-type ) . PHP will be used to generate a link to download a specific s3-object (and what’s the interest , you might think) , well , the link will have a custom expiration time (preferable a few minutes ) . A practical example could be an on-line store that sells downloadable goods (books , software applications ) , the purchaser will charge his/her credit card with the relevant amount ( for instance via PayPal) and after the successful process he/she will be redirected to the web-site of the purchased product to finish his/her shopping experience . Finally , a download link (which will expire in a predefined time ) will allow the buyer to acquire his ownership .
I have to admit , it wasn’t a simple task for me to write the code . Although the basic concepts seem simple to understand , the implementation wasn’t .The picture below is a flow-chart I drew to make the concepts more understandable by me . Finally , this code-snippet gave me a good starting point , I don’t now the name of the creator , but he certainly deserves a good citation . If someone happens to knows his name , let me know and I’ll update this article .
Prerequisites : The reader should already have an Amazon account (credit card is required for signing up , new members get a one year free AWS Free Usage Tier ) .
A basic understanding of PHP and Amazon’s web-services is also a requirement . I won’t even explain what the following keywords mean : awsAccessKey , awsSecretKey , bucketName , objectPath . If these aforementioned keywords sounds cryptic to you , this article will not make any sense to you . Try first to get familiar with these words before continuing reading . Promise , this article will stay in “published status” , until WordPress.com decides to shut-down the platform 🙂 .
My code examples will be presented in procedural and OOP (Object oriented) format , use the format that suits your needs . Firstly , some basic security concepts should be clarified before the code can be “digested” from all of you . Let’s get started …….
Amazon’s S3 REST API uses a custom HTTP scheme based on a keyed-HMAC (Hash Message Authentication Code) for authentication .To authenticate a request , you first concatenate selected elements of the request to form a string .You then use your AWS Secret Access Key to calculate the HMAC algorithm of that string , this output is named “the signature” because it simulates the security properties of a real signature (in technical jargon it is called “signing the request” ) . Finally, you add this signature as a parameter of the request (to the query-string) . When Amazon receives a request , it first deciphers (a decryption process) the signature and detect possible message tampering or forgery attempts . If the signature is found to be clear , the request is executed ( proceed to downloading ) . The following query-string designates a complete request , the parts colored in red define our custom query .
http://BUCKETNAME.s3.amazonaws.com/ OBJECTPATH? AWSAccessKeyId=YOURACCESSKEY& Expires=XXXXXXX& Signature=SIGNATURE http://domain.testbucket3.s3.amazonaws.com/ Object-oriented-programming.pdf? AWSAccessKeyId=KIAIAB5XG33FWWZ5FTTQ& Expires=1354642633& Signature=lEmztZuOjpbXCUKICSqs8oolA1k%3D
Description of the parts :
- BUCKETNAME : As you already know each S3 storage-bucket is designated by an unique name . Each bucket can have an hierarchic structure to organize it’s content . Like your computer does (folders inside other folders)
- OBJECTPATH : The name of the object in a bucket . If the object is stored into a hierarchical directory structure , OBJECTPATH must reflect the full path-name ie /dir1/dir2/dir3/book.pdf .
- YOURACCESSKEY : Developers are issued an AWS Access-Key ID and AWS SecretAccess Key when they register . Of-course only the Access-Key is made publicly visible (the Secret-Key is only used to calculate the signature — on both ends) .
- XXXXXXX : Expiration time in Unix time-stamp format .
- SIGNATURE : Firstly a complete query-string is composed (expires , bucket , objectpath) , then it’s passed into the HMAC-algorithm which is signed with your secret-Key . The result is an hashed string that designates your authentication signature . Your query string can travel over an un-secure channel (http) , every attempt for tampering or forging this string will be detected on the destination (Amazon’s REST API) and immediately rejected . Amazon uses a symmetric-key authentication mechanism (the same secret-key is used on both end’s ) , if you have any suspect of security issues , regenerate a new key-pair (through your Amazon’s administration panel) . It’s obviously that you should keep your secret-Key as your secret weapon .
The following picture demonstrates an attempt to forge the query-string , Amazon responded with a message (Access Denied) :
Procedural code :
<?php function s3TempLink($awsAccessKey, $awsSecretKey, $bucketName , $objectPath , $expires = 5) { // Calculating expiry time $expires = time() + ($expires * 60) ; $objectPath = ltrim($objectPath, '/') ; $signature = "GET\n\n\n$expires\n".'/'.$bucketName.'/'.$objectPath ; // Calculating HMAC-sha1 $hashedSignature = base64_encode(hash_hmac('sha1' ,$signature , $awsSecretKey , true )) ; // Constructing the URL $url = sprintf('http://%s.s3.amazonaws.com/%s', $bucketName , $objectPath); // Constructing the query String $queryString = http_build_query( array( 'AWSAccessKeyId' => $awsAccessKey , 'Expires' => $expires , 'Signature' => $hashedSignature )) ; // Apending query string to URL return $url.'?'.$queryString ; } $objectPath = "/Object-oriented-programming.pdf" ; $bucketName = "domain.testbucket3" ; $awsAccessKey = "Your Access Key" ; $awsSecretKey = "Your Secret Key" ; $s3URL = s3TempLink("$awsAccessKey" , "$awsSecretKey", "$bucketName", "$objectPath") ; echo $s3URL ; ?> <h3> Hello </h3> <p> thanks for purchasing this object , don't hesitate to ask questions if you need </p> <a href="<?php echo $s3URL ; ?>"> Download the object from this link </a>
Object Oriented code :
<?php class s3TempLink { private $objectPath ; private $bucketName ; private $awsAccessKey ; private $awsSecretKey ; private $expires ; private $accepted_options = array ( "objectPath" ,"bucketName" , "awsAccessKey" , "awsSecretKey" , "expires" ) ; public function __construct(array $credentials) { foreach ($credentials as $k => $v) { if (in_array("$k", $this->accepted_options)) { $this->{$k} = $v ; }else{ echo ("The parameter -- <strong>$k</strong> -- is unknow") ; exit() ; } // End if-in_array } // End foreach } // End of public function __construct public function getLink() { // Calculating expiry time $expires = time() + ($this->expires * 60) ; $objectPath = $this->objectPath ; $bucketName = $this->bucketName ; $objectPath = ltrim($objectPath, '/') ; $signature = "GET\n\n\n$expires\n".'/'.$bucketName.'/'.$objectPath ; // Calculating HMAC-sha1 $hashedSignature = base64_encode(hash_hmac('sha1' ,$signature , $this->awsSecretKey , true )) ; // Constructing the URL $url = sprintf('http://%s.s3.amazonaws.com/%s', $bucketName , $objectPath); // Constructing the query String $queryString = http_build_query( array( 'AWSAccessKeyId' => $this->awsAccessKey , 'Expires' => $expires , 'Signature' => $hashedSignature )) ; // Apending query string to URL return $url.'?'.$queryString ; } // End of public function getLink public function getExpires() { return $this->expires ; } // End of public function getExpires() } // End of Class ?> // Practical Example <?php error_reporting(0) ; // Include the "Class.s3TempLink.inc" file first require_once("Class.s3TempLink.inc") ; $config = parse_ini_file("config/aws_credentials.ini") ; $credentials = array( 'objectPath' => "$config[objectPath]", 'bucketName' => "$config[bucketName]" , 'awsAccessKey' => "$config[awsAccessKey]" , 'awsSecretKey' => "$config[awsSecretKey]" , 'expires' => "$config[expires]" ) ; $tempLink = new s3TempLink($credentials) ; ?> <a href="<?php echo $tempLink->getLink() ; ?>"> Download the content .Note: the link will expire in <?php echo $tempLink->getExpires() ; ?> minutes</a> // aws_credentials.ini ; Define your AWS credentials [aws] objectPath = "/direct-mail.jpg" bucketName = "domain.testbucket3" awsAccessKey = "Your Access Key" awsSecretKey = "Your Secret Key" expires = '5'
Final thoughts :
I can’t stress it enough , keep your secret weapon on a safe place . As it’s needed to generate your signature , it should be accessible on your web-server’s file-system . A minimum security measure would be to store it on a non-publicly accessible directory .
After I originally left a comment I seem to have clicked the -Notify me when new comments are added- checkbox and from now on every time a comment is added I recieve four emails with the exact same
comment. Is there a means you are able to remove
me from that service? Many thanks!
In the notification email , you should see a link to “Manage Subscriptions.” By clicking on that link, you will have the option to remove your subscription to the comments on that page .
Have a nice day .
It’s awesome to come across a blog every once in a while that isn’t the same unwanted rehashed material. Excellent read! I’ve savedyour site and I’m including your RSS feeds
Why aren’t you using the SDK and do:
$s3 = new AmazonS3();
$bucket = ‘mybucket’;
$tempUrl = $s3->get_object_url($bucket, ‘Object-oriented-programming.pdf’, ’10 days’);
(Make the object private.)
@Michael
Of course the SDK is the best solution for “heavy weight” interactions with Amazon’s infrastructure . For just a simple task , like the article’s subject , one could choose to use a 12-line function instead of installing a whole library .
Thanks for commenting .
ps It was my omission of not to mention the SDK in my article, and I plan to update it soon .
I like what you guys are up also. Such intelligent work and reporting! Carry on the excellent works guys I have incorporated you guys to my blogroll. I think it will improve the value of my site 🙂
Hi there, just became aware of your blog through Google, and
found that it is truly informative. I am gonna watch out for brussels.
I’ll appreciate if you continue this in future. Numerous people will be benefited from your writing. Cheers!
To avoid replays of your requests, AWS requires the time stamp in the request to be within 15 minutes of the AWS system time. To avoid clock synchronization errors, we recommend you fetch the current date from the CloudFront server and then use that as the time stamp for your request and the string for your signature.
It’s actually a nice and helpful piece of info. I am satisfied that you simply shared this useful information with us. Please stay us informed like this. Thank you for sharing.
If some one wants to be updated with latest technologies then he must be go to see this
site and be up to date daily.
Thanks so much for this tutorial. It was EXACTLY what I needed for a project I was working on in Laravel. Didn’t want to use the AWS package as the only feature I needed was the expiring urls. Saved me so much time! Thanks again!!!
@Rich
As I’m also a fan of Laravel 4 , let me know if you need help . Probably we could develop a L4 package .
Thanks! This saved me a headache. Your s3TempLink() function saved me from installing a library for a quick use.
@Jeremy
You are welcome 🙂
[…] runs a function that creates the signed AWS S3 expiring links. I have to thank Tournas Dimitrios https://tournasdimitrios1.wordpress.com/2012/12/04/how-to-create-expiring-links-for-amazons-s3-with-…. For his original work on the function to create the links. I just had a few tweaks to his function […]