• Document Up to Date

Setup CloudFront Signed Cookies in CrafterCMS

One way to provide access to restricted content through AWS CloudFront is to use signed cookies. This section details how to setup CloudFront signed cookies for CrafterCMS with SSO.

From the AWS documentation

CloudFront signed cookies allow you to control who can access your content when you don't want to change your current URLs or when you want to provide access to multiple restricted files, for example, all of the files in the subscribers' area of a website.

Here are the steps:

  1. Configure CloudFront to use signed cookies following this guide: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html

  2. Add the Groovy class to your site’s classes.

    CloudFrontUtils.groovy
     1package org.craftercms.aws.utils
     2
     3@Grapes(
     4    @Grab(group='com.amazonaws', module='aws-java-sdk-cloudfront', version='1.11.435', initClass = false)
     5)
     6
     7import java.util.Date;
     8import groovy.util.logging.Slf4j
     9
    10import javax.servlet.http.Cookie
    11
    12import com.amazonaws.auth.PEM;
    13import com.amazonaws.services.cloudfront.CloudFrontCookieSigner
    14import com.amazonaws.services.cloudfront.util.SignerUtils.Protocol
    15
    16@Slf4j
    17class CloudFrontUtils {
    18    
    19    static void setSignedCookies(request, response, siteConfig) {
    20        if (!signedCookiesExist(request)) {
    21            def protocol = Protocol.https;
    22            def domain = siteConfig.getString('aws.cloudFront.signedCookies.domain')
    23            def resourcePath = siteConfig.getString('aws.cloudFront.signedCookies.resourcePath')
    24            def keyPairId = siteConfig.getString('aws.cloudFront.signedCookies.keyPairId')
    25            def privateKeyContent = siteConfig.getString('aws.cloudFront.signedCookies.privateKey')
    26            def privateKey = PEM.readPrivateKey(new ByteArrayInputStream(privateKeyContent.getBytes('UTF-8')))
    27            def cloudFrontTimeToExpire = siteConfig.getLong('aws.cloudFront.signedCookies.cloudFrontTimeToExpire')
    28            def cloudFrontExpiresOn = new Date(System.currentTimeMillis() + (cloudFrontTimeToExpire * 60 * 1000))
    29            def cookieMaxAge = siteConfig.getLong('aws.cloudFront.signedCookies.cookieMaxAge') * 60
    30            def cookieSecure = true
    31            def cookiePath = '/'
    32            
    33            def cookies = CloudFrontCookieSigner.getCookiesForCustomPolicy(protocol, domain, privateKey, resourcePath, keyPairId, cloudFrontExpiresOn, null, null)
    34            
    35            def signatureCookie = new Cookie(cookies.signature.key, cookies.signature.value)
    36                signatureCookie.secure = cookieSecure
    37                signatureCookie.maxAge = cookieMaxAge
    38                signatureCookie.path = cookiePath
    39                
    40            def keyPairIdCookie = new Cookie(cookies.keyPairId.key, cookies.keyPairId.value)
    41                keyPairIdCookie.secure = cookieSecure
    42                keyPairIdCookie.maxAge = cookieMaxAge
    43                keyPairIdCookie.path = cookiePath
    44                
    45            def policyCookie = new Cookie(cookies.policy.key, cookies.policy.value)
    46                policyCookie.secure = cookieSecure
    47                policyCookie.maxAge = cookieMaxAge
    48                policyCookie.path = cookiePath
    49            
    50            response.addCookie(signatureCookie)
    51            response.addCookie(keyPairIdCookie)
    52            response.addCookie(policyCookie)
    53        }
    54    }
    55    
    56    static boolean signedCookiesExist(request) {
    57        def cookies = request.cookies
    58        for (int i = 0; i < cookies.length; i++) {
    59          if ('CloudFront-Key-Pair-Id' == cookies[i].name) {
    60              return true
    61          }
    62        }
    63        
    64        return false
    65    }
    66
    67}
    
  3. Create a Groovy filter that checks for current user authentication/authorization on the requests that need it, and then calls the class method: CloudFrontUtils.setSignedCookies(request, response, siteConfig)

  4. Add the following config to Engine’s site-config.xml:

     1<aws>
     2  <cloudFront>
     3    <signedCookies>
     4      <domain><!--- Site's domain name, used by CloudFront --></domain>
     5      <resourcePath>static-assets/*</resourcePath>
     6      <keyPairId encrypted=""><!-- ID of the key pair created in step 1, recommended to be encrypted with Encrypt Marked from the UI  --></keyPairId>
     7      <privateKey encrypted=""><!-- Content of the private key created in step 1, recommended to be encrypted with Encrypt Marked from the UI</privateKey>
     8      <cloudFrontTimeToExpire><!--Time in minutes after which CloudFront will not allow access to the content using the cookie --></cloudFrontTimeToExpire>
     9      <cookieMaxAge><!-- Time in minutes after which the browser will consider the cookie expired --></cookieMaxAge>
    10    </signedCookies>
    11  </cloudFront>
    12</aws>
    

  5. Configure an Error Page HTML in CloudFront for 403 errors, that will redirect to Engine using JS so that the SSO flow is started. It can be like the following:

    <!DOCTYPE html>
    <!-- saved from url=(0014)about:internet -->
    <html lang="en">
      <head>
        ...
        <script>
          if(document.location.hash.indexOf("dlink") == -1) {
            document.location = "/auth-asset?a=" + document.location.pathname + "#dlink";
          }
        </script>
        ...
      </head>
      <main id="main-content">
        <!-- PAGE CONTENT -->
        <script>
          if(document.location.hash.indexOf("dlink") != -1) {
            document.getElementById("headline").innerHTML = "403";
            document.getElementById("message").innerHTML = "You do not have permissions to access the requested resource. You will be redirected to the home page momentarily.";
            setTimeout(function(){ document.location = "/" }, 5000);
          }
        </script>
    </body></html>
    

  6. Create a /auth-asset page in your site with a Groovy script that only redirects back to the asset (the auth and cookie should have been already setup by filters):

    if(params.a) {
      response.sendRedirect(params.a)
    }