
{"id":207,"date":"2015-04-05T18:36:32","date_gmt":"2015-04-05T08:36:32","guid":{"rendered":"https:\/\/mnm.at\/markus\/?p=207"},"modified":"2015-10-10T16:47:57","modified_gmt":"2015-10-10T05:47:57","slug":"serving-private-content-through-cloudfront-using-signed-cookies","status":"publish","type":"post","link":"https:\/\/mnm.at\/markus\/2015\/04\/05\/serving-private-content-through-cloudfront-using-signed-cookies\/","title":{"rendered":"Serving Private Content Through CloudFront Using Signed Cookies"},"content":{"rendered":"<p>Only a few weeks back AWS&#8217; CloudFront announced the use of signed cookies to secure access to private content.<\/p>\n<p>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&#8217;t helping.<\/p>\n<p>The highlight: <strong>Do not use PHP setcookie<\/strong><\/p>\n<p>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 <code>header()<\/code> for that reason.<\/p>\n<p>Other more obvious things:<\/p>\n<ul>\n<li>Using a CNAME for your CDN makes everything easier and cleaner.<\/li>\n<li>Really read the <a href=\"http:\/\/docs.aws.amazon.com\/AmazonCloudFront\/latest\/DeveloperGuide\/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs\">documentation<\/a>.<\/li>\n<li>CloudFront uses its own private key. The CloudFront private keys are created per AWS account via your root account.<\/li>\n<li>When CloudFront asks for the account ID of the signer when you set up a distribution&#8217;s &#8216;Behavior&#8217;, it refers to that root account ID. You cannot add that account ID as a signer to the &#8216;Behavior&#8217; to your own distribution, instead it will use &#8220;Self&#8221; and ignore the account ID. This is fine.<\/li>\n<li>HTTPS\/HTTP re-directions can break everything.<\/li>\n<\/ul>\n<p>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.<\/p>\n<p>In my case I have not yet set up the CNAME nor got the SSL certificate for it. The browser would reject any &#8216;cloudfront.net&#8217; cookies that do not come from a &#8216;cloudfront.net&#8217; domain. Therefore I have a two step approach to be able to set a .cloudfront.net cookie.<\/p>\n<ul>\n<li>The CDN distribution dsmxmpl.cloudfront.net is set up with a &#8216;Behavior&#8217; that lets CloudFrontSignedCookieHelper pass through with all its headers and cookies and URL parameters to www.example.com and never caches it.<\/li>\n<li>All other requests for dsmxmpl.cloudfront.net are handled via a default &#8216;Behavior&#8217; 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.<\/li>\n<\/ul>\n<p>When a user loads a gallery the browser loads gallery.xml directly from example.com, this forces authentication.<\/p>\n<ul>\n<li>The returned gallery html includes a JS file reference to <code>\/\/dsmxmpl.cloudfront.net\/CloudFrontSignedCookieHelper.php<\/code>.<\/li>\n<li>CloudFrontSignedCookieHelper is loaded through dsmxmpl.cloudfront.net .<\/li>\n<li>dsmxmpl.cloudfront.net never caches that request and requests it from www.example.com .<\/li>\n<li>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 .<\/li>\n<li>These CDN signature cookies are passed through the CDN to the user&#8217;s browser.<\/li>\n<li>The user&#8217;s browser accepts these CDN signature cookies for dsmxmpl.cloudfront.net .<\/li>\n<li>For all future requests to dsmxmpl.cloudfront.net (in that browser session) the browser will sent these cookies on to dsmxmpl.cloudfront.net .<\/li>\n<li>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.<\/li>\n<\/ul>\n<p>The following CloudFrontSignedCookieHelper.php code would also work for the more appropriate CNAME scenario as part of the first authentication.<\/p>\n<p>Apart from <code>isAuthorized()<\/code> this is a working example. You can handle the authentication many different ways so I don&#8217;t elaborate.<\/p>\n<pre><code>&lt;?php\n\/** \n* site.secrets.php sets CLOUDFRONT_KEY_PAIR_ID and CLOUDFRONT_KEY_PATH as well as CDN_HOST \n* e.g.  \n\/\/ define('CLOUDFRONT_KEY_PAIR_ID' , 'APSOMEOROTHERA')\n\/\/ define('CLOUDFRONT_KEY_PATH' , '\/etc\/secrets\/pk.APSOMEOROTHERA.pem')\n\/\/ define('CDN_HOST' , 'dsmxmpl.cloudfront.net')\n*\/\nrequire_once ('\/etc\/secrets\/site.secrets.php');\n\nclass CloudFrontSignedCookieHelper {\n   public static function rsa_sha1_sign($policy, $private_key_filename) {\n      $signature = \"\";\n      openssl_sign ( $policy, $signature, file_get_contents ( $private_key_filename ) );\n      return $signature;\n   }\n   public static function url_safe_base64_encode($value) {\n      $encoded = base64_encode ( $value );\n      return str_replace ( array ('+','=','\/'), array ('-','_','~'), $encoded );\n   }\n   public static function getSignedPolicy($private_key_filename, $policy) {\n      $signature = CloudFrontSignedCookieHelper::rsa_sha1_sign ( $policy, $private_key_filename );\n      $encoded_signature = CloudFrontSignedCookieHelper::url_safe_base64_encode ( $signature );\n      return $encoded_signature;\n   }\n   public static function getNowPlus2HoursInUTC() {\n      $dt = new DateTime ( 'now', new DateTimeZone ( 'UTC' ) );\n      $dt-&gt;add ( new DateInterval ( 'P1D' ) );\n      return $dt-&gt;format ( 'U' );\n   }\n   public static function setCookie($name, $val, $domain) {\n      \/\/ using our own implementation because\n      \/\/ using php setcookie means the values are URL encoded and then AWS CF fails\n      header ( \"Set-Cookie: $name=$val; path=\/; domain=$domain; secure; httpOnly\", false );\n   }\n   public static function setCloudFrontCookies() {\n      $cloudFrontHost = CDN_HOST;\n      $cloudFrontCookieExpiry = CloudFrontSignedCookieHelper::getNowPlus2HoursInUTC ();\n      $customPolicy = '{\"Statement\":[{\"Resource\":\"https:\/\/' . $cloudFrontHost .\n            '\/*\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":' . $cloudFrontCookieExpiry . '}}}]}';\n      $encodedCustomPolicy = CloudFrontSignedCookieHelper::url_safe_base64_encode ( $customPolicy );\n      $customPolicySignature = CloudFrontSignedCookieHelper::getSignedPolicy ( CLOUDFRONT_KEY_PATH, \n            $customPolicy );\n      CloudFrontSignedCookieHelper::setCookie ( \"CloudFront-Policy\", $encodedCustomPolicy, $cloudFrontHost);\n      CloudFrontSignedCookieHelper::setCookie ( \"CloudFront-Signature\", $customPolicySignature, $cloudFrontHost);\n      CloudFrontSignedCookieHelper::setCookie ( \"CloudFront-Key-Pair-Id\", CLOUDFRONT_KEY_PAIR_ID, $cloudFrontHost);\n   }\n}\n\nif (isAuthorized()){\n   CloudFrontSignedCookieHelper::setCloudFrontCookies ();\n}\n?&gt;\nvar cloudFrontCookieSet=true;\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Making use of the new CloudFront signed cookie functionality <a href=\"https:\/\/mnm.at\/markus\/2015\/04\/05\/serving-private-content-through-cloudfront-using-signed-cookies\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":6,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_exactmetrics_skip_tracking":false,"_exactmetrics_sitenote_active":false,"_exactmetrics_sitenote_note":"","_exactmetrics_sitenote_category":0,"footnotes":""},"categories":[5],"tags":[12,30],"class_list":["post-207","post","type-post","status-publish","format-standard","hentry","category-tech","tag-aws","tag-php"],"_links":{"self":[{"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/posts\/207","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/comments?post=207"}],"version-history":[{"count":3,"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/posts\/207\/revisions"}],"predecessor-version":[{"id":216,"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/posts\/207\/revisions\/216"}],"wp:attachment":[{"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/media?parent=207"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/categories?post=207"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mnm.at\/markus\/wp-json\/wp\/v2\/tags?post=207"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}