Only a few weeks back AWS’ CloudFront announced the use of signed cookies to secure access to private content.
Getting signed cookies to work with CloudFront in my particular test scenario made me trip over a couple of foot falls that were partially caused by my lack of CloudFront knowledge and partially because the documentation wasn’t helping.
The highlight: Do not use PHP setcookie
PHP setcookie encodes the signed cookie and that will break the signature. The signature process already encodes the signature anyway. The below code example uses header()
for that reason.
Other more obvious things:
- Using a CNAME for your CDN makes everything easier and cleaner.
- Really read the documentation.
- CloudFront uses its own private key. The CloudFront private keys are created per AWS account via your root account.
- When CloudFront asks for the account ID of the signer when you set up a distribution’s ‘Behavior’, it refers to that root account ID. You cannot add that account ID as a signer to the ‘Behavior’ to your own distribution, instead it will use “Self” and ignore the account ID. This is fine.
- HTTPS/HTTP re-directions can break everything.
The simple case would be that you use a CNAME for CloudFront distribution which means that your website and your CloudFront distribution can share cookies.
In my case I have not yet set up the CNAME nor got the SSL certificate for it. The browser would reject any ‘cloudfront.net’ cookies that do not come from a ‘cloudfront.net’ domain. Therefore I have a two step approach to be able to set a .cloudfront.net cookie.
- The CDN distribution dsmxmpl.cloudfront.net is set up with a ‘Behavior’ that lets CloudFrontSignedCookieHelper pass through with all its headers and cookies and URL parameters to www.example.com and never caches it.
- All other requests for dsmxmpl.cloudfront.net are handled via a default ‘Behavior’ which only allows access with a signed cookie (or url). In my case that means all requests are passed on to S3 and cached forever. S3 itself is setup to only allows access from dsmxmpl.cloudfront.net.
When a user loads a gallery the browser loads gallery.xml directly from example.com, this forces authentication.
- The returned gallery html includes a JS file reference to
//dsmxmpl.cloudfront.net/CloudFrontSignedCookieHelper.php
. - CloudFrontSignedCookieHelper is loaded through dsmxmpl.cloudfront.net .
- dsmxmpl.cloudfront.net never caches that request and requests it from www.example.com .
- CloudFrontSignedCookieHelper on www.example.com checks if the user is authenticated and then creates the CDN signature cookies with a domain of dsmxmpl.cloudfront.net .
- These CDN signature cookies are passed through the CDN to the user’s browser.
- The user’s browser accepts these CDN signature cookies for dsmxmpl.cloudfront.net .
- For all future requests to dsmxmpl.cloudfront.net (in that browser session) the browser will sent these cookies on to dsmxmpl.cloudfront.net .
- The rest of the gallery html will trigger image requests to dsmxmpl.cloudfront.net . dsmxmpl.cloudfront.net will allow these requests because the signed cookie is allowing access.
The following CloudFrontSignedCookieHelper.php code would also work for the more appropriate CNAME scenario as part of the first authentication.
Apart from isAuthorized()
this is a working example. You can handle the authentication many different ways so I don’t elaborate.
<?php
/**
* site.secrets.php sets CLOUDFRONT_KEY_PAIR_ID and CLOUDFRONT_KEY_PATH as well as CDN_HOST
* e.g.
// define('CLOUDFRONT_KEY_PAIR_ID' , 'APSOMEOROTHERA')
// define('CLOUDFRONT_KEY_PATH' , '/etc/secrets/pk.APSOMEOROTHERA.pem')
// define('CDN_HOST' , 'dsmxmpl.cloudfront.net')
*/
require_once ('/etc/secrets/site.secrets.php');
class CloudFrontSignedCookieHelper {
public static function rsa_sha1_sign($policy, $private_key_filename) {
$signature = "";
openssl_sign ( $policy, $signature, file_get_contents ( $private_key_filename ) );
return $signature;
}
public static function url_safe_base64_encode($value) {
$encoded = base64_encode ( $value );
return str_replace ( array ('+','=','/'), array ('-','_','~'), $encoded );
}
public static function getSignedPolicy($private_key_filename, $policy) {
$signature = CloudFrontSignedCookieHelper::rsa_sha1_sign ( $policy, $private_key_filename );
$encoded_signature = CloudFrontSignedCookieHelper::url_safe_base64_encode ( $signature );
return $encoded_signature;
}
public static function getNowPlus2HoursInUTC() {
$dt = new DateTime ( 'now', new DateTimeZone ( 'UTC' ) );
$dt->add ( new DateInterval ( 'P1D' ) );
return $dt->format ( 'U' );
}
public static function setCookie($name, $val, $domain) {
// using our own implementation because
// using php setcookie means the values are URL encoded and then AWS CF fails
header ( "Set-Cookie: $name=$val; path=/; domain=$domain; secure; httpOnly", false );
}
public static function setCloudFrontCookies() {
$cloudFrontHost = CDN_HOST;
$cloudFrontCookieExpiry = CloudFrontSignedCookieHelper::getNowPlus2HoursInUTC ();
$customPolicy = '{"Statement":[{"Resource":"https://' . $cloudFrontHost .
'/*","Condition":{"DateLessThan":{"AWS:EpochTime":' . $cloudFrontCookieExpiry . '}}}]}';
$encodedCustomPolicy = CloudFrontSignedCookieHelper::url_safe_base64_encode ( $customPolicy );
$customPolicySignature = CloudFrontSignedCookieHelper::getSignedPolicy ( CLOUDFRONT_KEY_PATH,
$customPolicy );
CloudFrontSignedCookieHelper::setCookie ( "CloudFront-Policy", $encodedCustomPolicy, $cloudFrontHost);
CloudFrontSignedCookieHelper::setCookie ( "CloudFront-Signature", $customPolicySignature, $cloudFrontHost);
CloudFrontSignedCookieHelper::setCookie ( "CloudFront-Key-Pair-Id", CLOUDFRONT_KEY_PAIR_ID, $cloudFrontHost);
}
}
if (isAuthorized()){
CloudFrontSignedCookieHelper::setCloudFrontCookies ();
}
?>
var cloudFrontCookieSet=true;
Dear Markus,
I’ve followed your instructions but I’m not able to have it working.
Currently I’ve my server with Nginx and a self-signed certificate. Here CloudFront won’t connect via HTTPS no matter what.
If I switch back to plain HTTP it connects but the cookies that I got are not supposed to work with CloudFront as an authentication (if you were wondering: they don’t).
So I stuck with an HTTPS that is not connecting and an HTTP that doesn’t work.
Can you please give me any advice, please?
Thank you
Luca
Way too late, a mis-configured spam filter swallowed/hid this an other comments.
In my example code I am setting the cookies with the ‘secure’ flag, but I would have thought that should only affect the connection between browser and CloudFront Therefore, even thought that would be bad practise, I would think that you can set up CloudFront to use http and still use https towards the browser.
Regarding self-signed certificate, I remember reading in the CloudFront documentation that self-signed certificates will not work with CloudFront.
Pingback: AWS Cloudfront SetCookie in PHP - DexPage
How you made to forward request to other server to execute php file form CF, will you please provide that info?
Very useful blog. 🙂
Same as with other comments, I missed yours until two days ago.
On my server I have a rule that whenever a (browser) request accesses anything under a gallery context (e.g. gallery/2013/MyGreatTripToMars/gallery.xml gallery/2013/MyGreatTripToMars/ or gallery/2013/MyGreatTripToMars/index.html) I do an authentication dance and afterwards I return an html which also includes a script reference to a the cookie setter like e.g. so:
The CloudFront uis set up to never cache this request and forwards that to my server. My server makes sure that the request is authenticated and sets CloudFront cookies and returns an empty response. CloudFront returns the whole things response and headers to the browser and the response cookie looks as if it comes from CloudFront (since they are coming from CF)
Hey buddy,
thanks for this great article. it helped me a lot!
Thanks for this article, it sure helped me. I’m now using a slightly different approach with the official AWS SDK for PHP, see my blog post “Using CloudFront Signed Cookies with the AWS SDK for PHP” 1.