Tim's Weblog
Tim Strehle’s links and thoughts on Web apps, software development and Digital Asset Management, since 2002.
2015-11-06

Using CloudFront Signed Cookies with the AWS SDK for PHP

If you store files on Amazon AWS S3 and want to restrict access to them, you can do so by setting up an AWS CloudFront distribution with either Signed URLs or Signed Cookies. Signed Cookies are interesting if you want to use static file URLs and just make sure only people logged into your application can access the files. (But think twice which approach to use; I ended up going back to Signed URLs because I cannot set cookies in each scenario.)

The setup can look like this – note the use of a custom subdomain (a CNAME in your DNS) to access CloudFront to make sure the application can set cookies that reach CloudFront (using HTTPS with a custom subdomain requires extra hoops, by the way) – click to enlarge:

With the help of the CloudFront documentation on Signed Cookies, Frederick Cheung’s Using Cloudfront Signed Cookies and Markus Ebenhoeh’s Serving Private Content Through CloudFront Using Signed Cookies, I managed to make it work. Unlike that last article, I’m using the official AWS SDK for PHP, and I had no problem using PHP’s setcookie() function.

Here’s my code:

<?php

// Settings

$access_key_id = 'ABCDEFG';
$secret_access_key = 'abcdefg123456';
$region = 'eu-central-1';
$key_pair_id = 'UVWXYZ';
$private_key = '/path/to/aws-cloudfront-pk-UVWXYZ.pem';
$cloudfront_base_url = 'http://files.example.com/basedir/';
$allowed_resource = $cloudfront_base_url . '*';
$policy_expires_after = (86400 * 7); // 1 week

// Initialize AWS SDK for PHP

require_once '/path/to/aws_sdk/aws-autoloader.php';

$sdk_params =
[
    'region' => $region,
    'version' => 'latest',
    'credentials' => new \Aws\Credentials\Credentials
    (
        $access_key_id, 
        $secret_access_key
    )
];

$sdk = new \Aws\Sdk($sdk_params);        

// CloudFront cookie #1: CloudFront-Key-Pair-Id

$cookies = [ ];

$cookies[ 'CloudFront-Key-Pair-Id' ] = $key_pair_id;

// CloudFront cookie #2: CloudFront-Policy

$policy =
[
   'Statement' => 
   [
      [
         'Resource' => $allowed_resource,
         'Condition' =>
         [
            'DateLessThan' => 
            [ 
                'AWS:EpochTime' => (time() + $policy_expires_after)
            ]
         ]
      ]
   ]
];

$policy = json_encode($policy);

$cookies[ 'CloudFront-Policy' ] = base64_encode($policy);

// CloudFront cookie #3: CloudFront-Signature

$cloudfront = $sdk->createCloudFront();

$url_signing_params =
[
    'url' => $allowed_resource,
    'policy' => $policy,
    'key_pair_id' => $key_pair_id,
    'private_key' => $private_key
];

$signed_url = $cloudfront->getSignedUrl($url_signing_params);

// We just need the "Signature" query string part of the signed URL

parse_str(parse_url($signed_url, PHP_URL_QUERY), $signed_url_arr);
$signature = $signed_url_arr[ 'Signature' ];

$cookies[ 'CloudFront-Signature' ] = $signature;

// Set cookies

// We're on app.example.com, files are on files.example.com
// (a CNAME we set up in our DNS).
// Set cookies for .example.com to that they're sent to the CloudFront
// subdomain.

// test.www.example.com => .example.com
// XXX bug: www.example.com.au => .com.au, which you cannot set cookies for 

$host_parts = array_reverse
(
    explode('.', parse_url($cloudfront_base_url, PHP_URL_HOST))
);

$cookie_domain = sprintf('.%s.%s', $host_parts[ 1 ], $host_parts[ 0 ]);

$cookie_path = parse_url($cloudfront_base_url, PHP_URL_PATH);

foreach ($cookies as $cookie_name => $cookie_value)
{
    setcookie($cookie_name, $cookie_value, 0, $cookie_path, $cookie_domain);
}