Setup CloudFront Signed Cookies in Crafter CMS

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 Crafter CMS 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
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    package org.craftercms.aws.utils
    
    @Grapes(
        @Grab(group='com.amazonaws', module='aws-java-sdk-cloudfront', version='1.11.435', initClass = false)
    )
    
    import java.util.Date;
    import groovy.util.logging.Slf4j
    
    import javax.servlet.http.Cookie
    
    import com.amazonaws.auth.PEM;
    import com.amazonaws.services.cloudfront.CloudFrontCookieSigner
    import com.amazonaws.services.cloudfront.util.SignerUtils.Protocol
    
    @Slf4j
    class CloudFrontUtils {
        
        static void setSignedCookies(request, response, siteConfig) {
            if (!signedCookiesExist(request)) {
                def protocol = Protocol.https;
                def domain = siteConfig.getString('aws.cloudFront.signedCookies.domain')
                def resourcePath = siteConfig.getString('aws.cloudFront.signedCookies.resourcePath')
                def keyPairId = siteConfig.getString('aws.cloudFront.signedCookies.keyPairId')
                def privateKeyContent = siteConfig.getString('aws.cloudFront.signedCookies.privateKey')
                def privateKey = PEM.readPrivateKey(new ByteArrayInputStream(privateKeyContent.getBytes('UTF-8')))
                def cloudFrontTimeToExpire = siteConfig.getLong('aws.cloudFront.signedCookies.cloudFrontTimeToExpire')
                def cloudFrontExpiresOn = new Date(System.currentTimeMillis() + (cloudFrontTimeToExpire * 60 * 1000))
                def cookieMaxAge = siteConfig.getLong('aws.cloudFront.signedCookies.cookieMaxAge') * 60
                def cookieSecure = true
                def cookiePath = '/'
                
                def cookies = CloudFrontCookieSigner.getCookiesForCustomPolicy(protocol, domain, privateKey, resourcePath, keyPairId, cloudFrontExpiresOn, null, null)
                
                def signatureCookie = new Cookie(cookies.signature.key, cookies.signature.value)
                    signatureCookie.secure = cookieSecure
                    signatureCookie.maxAge = cookieMaxAge
                    signatureCookie.path = cookiePath
                    
                def keyPairIdCookie = new Cookie(cookies.keyPairId.key, cookies.keyPairId.value)
                    keyPairIdCookie.secure = cookieSecure
                    keyPairIdCookie.maxAge = cookieMaxAge
                    keyPairIdCookie.path = cookiePath
                    
                def policyCookie = new Cookie(cookies.policy.key, cookies.policy.value)
                    policyCookie.secure = cookieSecure
                    policyCookie.maxAge = cookieMaxAge
                    policyCookie.path = cookiePath
                
                response.addCookie(signatureCookie)
                response.addCookie(keyPairIdCookie)
                response.addCookie(policyCookie)
            }
        }
        
        static boolean signedCookiesExist(request) {
            def cookies = request.cookies
            for (int i = 0; i < cookies.length; i++) {
              if ('CloudFront-Key-Pair-Id' == cookies[i].name) {
                  return true
              }
            }
            
            return false
        }
    
    }
    
  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
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <aws>
      <cloudFront>
        <signedCookies>
          <domain><!--- Site's domain name, used by CloudFront --></domain>
          <resourcePath>static-assets/*</resourcePath>
          <keyPairId encrypted=""><!-- ID of the key pair created in step 1, recommended to be encrypted with Encrypt Marked from the UI  --></keyPairId>
          <privateKey encrypted=""><!-- Content of the private key created in step 1, recommended to be encrypted with Encrypt Marked from the UI</privateKey>
          <cloudFrontTimeToExpire><!--Time in minutes after which CloudFront will not allow access to the content using the cookie --></cloudFrontTimeToExpire>
          <cookieMaxAge><!-- Time in minutes after which the browser will consider the cookie expired --></cookieMaxAge>
        </signedCookies>
      </cloudFront>
    </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)
    }