Serving private content in NextJS/NodeJS using AWS CloudFront and signed URLs and cookies
Serving private content in NextJS/NodeJS using AWS CloudFront and signed URLs and cookies

Serving private content in NextJS/NodeJS using AWS CloudFront and signed URLs and cookies

You might find yourself in a situation in your NextJS or NodeJS development where you would like to serve private content (images, videos, etc) only to authenticated users. You might also want to further allow access only to a subset of files within an Amazon S3 bucket (delivered through CloudFront) for a given authorized user.

AWS CloudFront provides two means by which private content may be served: via signed URLs and signed cookies.

Signed URLs allow you to restrict access to individual files. Thus, a unique signed URL must be generated on each request for each file within your S3 bucket. Signed cookies on the other hand allow the signing of whole directories of files using a cookie which is then sent on the request of files within a restricted directory. Signed cookies are particularly relevant for serving video files in the HLS (HTTP Live Streaming) format where a video consists of a .m3u8 file and a sequence of .ts files.

Let us go through the steps to using signed URLs and cookies. 

Generating a public-private key pair:

The first step in the process is to generate a key pair using the OpenSSL tool. The private key is used to sign the URL or cookies and, CloudFront verifies the signature with the public key. Use these commands to generate and extract keys.

% openssl genrsa -out cloudfront_private_key.pem 2048

The command above will generate an RSA key pair named ‘cloudfront_private_key’ with length 2048 bits. This key contains both the private and public keys. The public key must be extracted from the generated key.

% openssl rsa -pubout -in cloudfront_private_key.pem -out cloudfront_public_key.pem

 

Upload generated public key to CloudFront

The extracted public key must afterward be uploaded to AWS CloudFront. In the AWS web console, go to CloudFront and add a new key to your list of public keys (if any).

Provide a key name to identify the public key and in the key value, paste the contents of the public key. 

 

 

 

 

 

 

Now create a new key group and add the public key you have just created to that key group.

It is now time to use the private key (which you must store securely in your NextJS or NodeJS server in an environment variable available only to the server) to sign your URLs and cookies.

 

 

 

 

Restrict access to S3 bucket using CloudFront

If you are not already serving your S3 private content with CloudFront, then now is the time to create a CloudFront distribution and restrict access to the S3 bucket. In the Cache behaviour settings, select ‘Yes’ for restrict viewer access and select the key group you created as the trusted key group.

Generate signed URL and cookies in NextJS/NodeJS

Set the content of the private key as the value of a chosen environment variable name:

CLOUDFRONT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n....."
CLOUDFRONT_PUBLIC_KEY_ID='xxxxxxx'

Where the CLOUDFRONT_PUBLIC_KEY_ID is the unique id of the public key you uploaded to CloudFront (this information can be found in the CloudFront web console). Recall to replace all new lines in the private key with \n within the environment variable value.

The code sample below creates a signed URL given an unsigned URL (within NodeJS server or in the getInitialProps/getServerSideProps functions in a NextJS app), using a custom policy:

import CloudFront from 'aws-sdk/clients/cloudfront';

const cloudFront = new CloudFront.Signer(
                             process.env.CLOUDFRONT_PUBLIC_KEY_ID,
                             process.env.CLOUDFRONT_PRIVATE_KEY.replace(/\\n/g, '\n')
                      )

const unsigned_url = "https://xyz.cloudfront.net/folder/file"
const policy = JSON.stringify(
                  {
                     Statement: [
                        {
                           Resource: unsigned_url,
                           Condition: {
                              DateLessThan: {
                                 "AWS:EpochTime": Math.floor((new          Date()).getTime() / 1000) + (60 * 60 * 24) // Current Time in UTC + time in seconds, (60 * 60 * 24 = 24 hours)
                              }
                           }
                        }
                     ]
                  });

 const signed_url = cloudFront.getSignedUrl({
                   url: unsigned_url,
                   policy: policy
                })

The next sample code creates a signed cookie that permits access to all files in a folder for a given user session.

import CloudFront from 'aws-sdk/clients/cloudfront';

const cloudFront = new CloudFront.Signer(
                             process.env.CLOUDFRONT_PUBLIC_KEY_ID,
                             process.env.CLOUDFRONT_PRIVATE_KEY.replace(/\\n/g, '\n')
                      )

const unsigned_folder = "https://xyz.cloudfront.net/folder/*"

const policy = JSON.stringify(
                  {
                     Statement: [
                        {
                           Resource: unsigned_folder,
                           Condition: {
                              DateLessThan: {
                                 "AWS:EpochTime": Math.floor((new          Date()).getTime() / 1000) + (60 * 60 * 24) // Current Time in UTC + time in seconds, (60 * 60 * 24 = 24 hours)
                              }
                           }
                        }
                     ]
                  });

const cookie = cloudFront.getSignedCookie({
                            policy,
                        });

const cookies = new Cookies(req, res)
                        
cookies.set('CloudFront-Key-Pair-Id', cookie['CloudFront-Key-Pair-Id'], {
             domain: <domain>,
             httpOnly: false
         });
                        
cookies.set('CloudFront-Policy', cookie['CloudFront-Policy'], {
             domain: <domain>,
             httpOnly: false
         });
                        
cookies.set('CloudFront-Signature', cookie['CloudFront-Signature'], {
             domain: <domain>,
             httpOnly: false
         });

Now all files within the signed folder will be accessible to the user.

Let me know in the comments section if you have any questions.

Further Readings:

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs

Published By
Evans Boateng Owusu
Evans is a Computer Engineer and cloud technology enthusiast. He has a Masters degree in Embedded Systems (focusing on Software design) from the Technical University of Eindhoven (The Netherlands) and a Bachelor of Science in Electronic and Computer Engineering from the Polytechnic University of Turin (Italy). In addition, he has worked for the high-tech industry in the the Netherlands and other large corporations for over seven years.... Show more
Comment