jump to navigation

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 .

hmac algorithm

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 : awsAccessKeyawsSecretKeybucketNameobjectPath . 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) :

Amazon s3 failed authentication

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 .

Comments»

1. reveiw site - December 5, 2012

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!

tournasdimitrios1 - December 5, 2012

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 .

2. grupa - December 13, 2012

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

3. Michael - December 19, 2012

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.)

tournasdimitrios1 - December 19, 2012

@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 .

4. hurtige - December 22, 2012

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 🙂

5. Toe - December 25, 2012

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!

6. silver - December 27, 2012

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.

7. Handy - January 2, 2013

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.

8. saul - April 7, 2013

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.

9. Rich - October 27, 2013

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!!!

tournasdimitrios1 - October 27, 2013

@Rich
As I’m also a fan of Laravel 4 , let me know if you need help . Probably we could develop a L4 package .

10. Jeremy - July 8, 2014

Thanks! This saved me a headache. Your s3TempLink() function saved me from installing a library for a quick use.

tournasdimitrios1 - July 8, 2014

@Jeremy
You are welcome 🙂

11. PHP Generated Feed With Expiring AWS S3 Links | Mark P. Sullivan - March 15, 2015

[…] 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-&#8230;. For his original work on the function to create the links. I just had a few tweaks to his function […]


Leave a comment