Node.js — Serve Private Content using AWS CloudFront

As I told you in one of my previous article that I ended up using CloudFront to serve my content, I thought to write a separate blog on CloudFront because I also struggled a lot to put things in right place because of very poor documentation for Node.js/JavaScript.

Node.js — Serve Private Content using AWS CloudFront

As I told you in one of my previous article that I ended up using CloudFront to serve my content, I thought to write a separate blog on CloudFront because I also struggled a lot to put things in right place because of very poor documentation for Node.js/JavaScript.


In my previous article, I was directly serving my private content from S3 bucket. But it was too slow for my project. Also if I use getObject in S3, I wasn’t able to cache the data. So I finally decided to shift to CloudFront.

But I found one good thing which S3 provides but CloudFront doesn’t, which is the support for AWS STS (read my previous article for all the details). In CloudFront, there is no support for STS till now, and also we have to generate the keys separately for CloudFront and the maximum limit for generating these keys is 100 per account. So let’s begin.


What we’ll do

  • See what CloudFront is
  • Setup CloudFront keys
  • Setup S3 bucket with CloudFront
  • Write Code for Signed URLs
    * Using Canned Policy
    * Using Custom Policy
  • Write Code for Signed Cookies
    * Using Canned Policy
    * Using Custom Policy
  • Setup Frontend
    * Setup Axios and fetch API.
  • Some configurations to save the cost

CloudFront Overview

As per the CloudFront webpage,

Amazon CloudFront is a fast content delivery network (CDN) service that securely delivers data, videos, applications, and APIs to customers globally with low latency, high transfer speeds.

Also, you can use CloudFront to serve your React, Vue or any other app which uses static content. If you use CloudFront to serve your app, you don’t have to worry about the scaling issues. You only have to pay for the per request charges. Yes, CloudFront alone is not special. Any CDN does this task but it is good to know other use cases.

This section is just to give you a basic idea on what CloudFront service really is if you don’t already know. If you want to know more you can simply Google for CDNs or CloudFront.


Generate CloudFront keys

  • Go to your AWS console and go to Security Credentials.
  • If you have admin or some similar rights, you’ll see the options for CloudFront key pairs.
  • Click on Create New Key Pair and download the private and public keys. Also, note the public access key.

Setup S3 bucket with CloudFront

  • Go to the CloudFront console and create a new distribution.
  • Select Web or RTMP as per your requirements. I was serving my content over Web so I chose Web.
  • In Origin settings, select your S3 bucket and it’ll automatically set the Origin ID.
  • If you want to serve the whole S3 bucket over CloudFront, leave the Origin Path field blank. Else you can specify the path to the folder which you want to serve over CloudFront.
  • Also, if you want to protect your content by allowing only those requests which include some particular headers, you can specify those headers in Origin Custom Headers option.
  • The most important thing in Default Cache Behavior Settings is to set Restrict Viewer Access to “Yes”. This will allow us to make requests using Signed URLs or Signed Cookies.
  • Set the other options according to your requirements.

Before Jumping into Signed URLs or Signed Cookies let’s see how CloudFront differentiates it.

As per their documentation,

If you use both signed URLs and signed cookies to control access to the same files and a viewer uses a signed URL to request a file, CloudFront determines whether to return the file to the viewer based only on the signed URL.

Which means if you provide Signed URL params to your content’s URL, it won’t include cookies in its request and it’ll treat the URL based on Signed URL params.

You should also check out this page if you don’t know what to choose and you’re confused between these two options.


Generate Signed URLs in Node.js

Let’s assume that you’ve left the Origin Path in Origin settings blank while creating a CloudFront distribution, and there is a folder named test and it contains 3 files a.txt, b.txt, c.txt.

Using Canned Policy

In Canned Policy, you have to assign a full path of your file to generate signed URLs. You can’t assign https://abcdefg.cloudfront.net/test/* to the url key in getSignedUrl.

This policy is very straightforward and it requires less effort to implement it but it is not as powerful/flexible as the Custom Policy. You can add as many parameters as you want in Custom Policy. You can find those in the CloudFront’s documentation.

Using Custom Policy

As you can see in the gist that we have only specified our policy and not the url or expires key because they’re already included in the custom policy.

But if you want to apply this policy for all the objects in CloudFront, simply change the Resource to:

https://abcdefghijklmn.cloudfront.net/*

Or, for only the test folder:

https://abcdefghijklmn.cloudfront.net/test/*

But this time you have to specify the url key inside getSignedUrl

Same for the expires. It is your choice to pass expiry time inside the policy or assign it explicitly inside getSignedUrl.

You can also explore different conditions for your custom policy in CloudFront’s documentation.

Generate Signed Cookies in Node.js

In this section, to set signed cookies, we’ll use Express.js.

When I started using Signed Cookies for the first time, I found that many browsers do not support setting cookies on *.cloudfront.net. So I just assigned a custom subdomain to the CloudFront URL. To do so, simply add a CNAME record of your subdomain pointing to your CloudFront URL. If you want to assign SSL to your custom domain/subdomain, read this blog.

For Separate frontends like React/Vue/Angular etc.

If you’re planning to set cookies for the frontend which isn’t being rendered by Express or any other backend, you have to configure your CORS like this:

app.use(cors({ origin: 'your-domain.com', credentials: true }));

Here, origin key takes the domain name which is going to be used by your frontend, or in other words, it will take the input from which it expects to receive the request from to frontend to set the cookies.

If you don’t want to set this credentials: true in all the routes, you can also set it on some specific routes. Check the CORS documentation for different backends.

Using Canned Policy

In Canned Policy, it will generate a JSON object having 3 keys

  • CloudFront-Key-Pair-Id
  • CloudFront-Signature
  • CloudFront-Expires

We can simply set the cookies by res.cookie in Express.js

Using Custom Policy

In my case, I generally want to generate cookies from which I can access all the content of the CloudFront distribution, or at least I can access a folder/folders. I don’t want to set cookies for each and every file. So the Custom Policy is the best option so far for me to implement in my app.

This also generates a JSON object which contains 3 keys but instead of CloudFront-Expires it generates CloudFront-Policy.

Unlike the Custom Policies in Signed URLs, you don’t need to specify a URL if you want to contain all the matches in Resource key. (e.g. http*://cdn.your-domain.com/*)

Setup Frontend to set Cookies

This part is optional for those who are using React/Vue/Angular or any other frontend framework (even if not using any frameworks) and want to fetch the cookies using HTTP or REST APIs using fetch or Axios.

Using Fetch API

In fetch you have to add credentials: 'include' or credentials: 'same-origin' as per your requirements in its options parameter.

You can explore more about that in its documentation.

Using Axios

There are several methods to add this credentials flag in Axios’s options which you can find in its documentation. I’ll show an example to achieve that.

Some Configurations to Save the Costs

As I mentioned above, we’ve to pay the charges per request. By default, CloudFront doesn’t let your browser cache the content of your requests in neither Signed URLs or Signed Cookies because of course, they are here to earn money. But they’re not preventing us to set the cache headers to let the browsers cache our content.

Go to your S3 bucket and select the folder/folders which you want to be cached inside the browsers and in Actions, select Change metadata.

Select Cache-Control option and set the max-age (in seconds).

E.g.: If you want the content to be cached for 7 days, set the Cache-Control to max-age=604800 (60 seconds * 60 minutes * 24 hours * 7 days)

For some more details to save the costs see this article

Footnotes

Though everything is there in the JavaScript aws-sdk, we’ve to struggle a lot to implement these things because of the poor documentation. Let me know if you find any bug or error in my code or there is anything you don’t understand. Also, you can ask me to write a blog on some other Services of any platforms like GCloud or Azure.

References