Build a password-protected website using AWS CloudFront, Lambda and S3

Image for post
Image for post

In a previous blog post I talked about how you can use AWS S3 to host a static website. While that approach is efficient and cost-effective, it doesn’t allow you to protect your website with a username and password. In the post, we’ll talk about how to add CloudFront and a Lambda function to your S3 website in order to provide greater security.

Start by preparing your website code using javascript, css, and html. I’ll be using this github repository to host my code for a simple website. Start by forking and cloning the files to your laptop.

Fire up the AWS Console. Make sure that you’re in the Northern Virginia region, as that’s the only region that currently supports Lambda@Edge.

Just as we did in the previous blog post, upload all of your files to an S3 bucket. This time, however, don’t make the bucket open to the public: leave all of the default settings as-is, i.e. “block all public access.” There’s no need for a bucket policy this time; you can leave that blank.

Image for post
Image for post

Next you’ll want to create your CloudFront distribution. The following points are taken from this excellent blog post.

Amazon CloudFront is a web service that speeds up distribution of your static and dynamic web content, such as . html, . css, . js, and image files, to your users. CloudFront delivers your content through a worldwide network of data centers called edge locations. You can read more about it here.

In the AWS Console, search for CloudFront and select “Create Distribution”. When prompted to select a delivery method, choose “Web” and then “Get Started”.

Image for post
Image for post

In the “Create Distribution” dialogue box, there are a lot of optional settings. Leave most of these just as they are. You should only update the following items.

Under “Origin Settings” select your S3 bucket from the dropdown list of “Origin Domain Name”. Select “Restrict Bucket Access” and “Create a New Identity” for the Origin Access Identity option, as shown below. Choose “Yes, Update Bucket Policy” for the Grant Read Permissions on Bucket option.

Image for post
Image for post

Set the Default Root Object as index.html (or whatever you named your .html file) and then choose “Create Distribution.” You’ll receive a temporary message that says “CloudFront Private Content Getting Started.”

Image for post
Image for post

It will take a couple of minutes for your distribution to be deployed; during this time its status will be displayed as “In Progress”. When finished, the status will switch to “Deployed.”

Image for post
Image for post

In the AWS Console, switch to Lambda. This is where we’ll build a function with username and password authorization, and then attach it to our CloudFront distribution. Most of the information here is taken from a very helpful response in Stack Overflow.

In the Lambda console, choose “Create function.” Create your Lambda from scratch and give it a name. Set your Runtime as Node.js 12.x . Don’t click “Create function” just yet.

Image for post
Image for post

Select the arrow by “Choose or create an execution role” and then “Create a new role from AWS policy templates.” Give the role a name. From Policy Templates select “Basic Lambda@Edge permissions (for CloudFront trigger)” and the click “Create function” as follows:

Image for post
Image for post

Once your Lambda is created take the following code and paste it in to the index.js file of the Function Code section - you can update the username and password you want to use by changing the authUser and authPass variables. Then click “Save”.

'use strict';
exports.handler = (event, context, callback) => {

// Get request and request headers
const request = event.Records[0].cf.request;
const headers = request.headers;

// Configure authentication
const authUser = 'user';
const authPass = 'pass';

// Construct the Basic Auth string
const authString = 'Basic ' + new Buffer(authUser + ':' + authPass).toString('base64');

// Require Basic authentication
if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
const body = 'Unauthorized';
const response = {
status: '401',
statusDescription: 'Unauthorized',
body: body,
headers: {
'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
},
};
callback(null, response);
}

// Continue request processing if authentication passed
callback(null, request);
};

This is what that looks like in the Lambda console:

Image for post
Image for post

In the upper menu, select Actions -> Deploy to Lambda@Edge as follows. Note that this will currently only work if you are in the Northern Virginia region (Lambda@Edge is still in beta).

Image for post
Image for post

In the dialogue box that pops up, select the CloudFront distribution you created earlier from the select menu. (Note: if you have done this before, you’ll probably have several CloudFront distributions to choose from. Lambda@Edge will default to the first, so you may have to change this to the correct distribution). Leave the Cache Behavior as *, and for the CloudFront Event select “Viewer Request”. Toggle “Include Body” and toggle “Confirm deploy to Lambda@Edge”. Finally, click “Deploy”.

Image for post
Image for post

It takes about 20 minutes to replicate your Lambda@Edge across all regions and edge locations. While it’s being replicated, the following message will display in your Lambda console:

Image for post
Image for post

Now switch back over to the CloudFront console. By this time, your distribution should be done deploying. Click on it, and you’ll see a set of descriptions under the “General” tab. Look for “Domain Name”: this is the URL of your new website. Copy and paste it into your browser (for my example, the URL is this: http://d22rkmcvhdxvoh.cloudfront.net/).

Image for post
Image for post

You should now be prompted to enter the username and password, which we defined earlier as “user” and “pass”.

You should now be able to view your website successfully:

Image for post
Image for post

Before you go, there’s one more thing to consider. You may want to make updates to your index.html file, and each time you do, you’ll need to upload the new version to your S3 bucket. However, you’ll quickly discover that your updates don’t seem to populate out to your CloudFront distribution right away — it may take them up to 24 hours. The problem with using CloudFront is that your users are seeing cached content (cached by AWS, not cached in their browser). If your request lands at an edge location that served the Amazon S3 response within 24 hours, CloudFront uses the cached response even if you updated the content in Amazon S3.

The solution is to remove the content from the CloudFront distribution’s cache by using an invalidation. In the description of your distribution, find the tab on the far right called “Invalidations”:

Image for post
Image for post

Click “Create Invalidation” and then in the dialogue box add /index.html and the click “Invalidate”. (note: if you have changed more than just that one file, you can just put /* and it will invalidate everything).

Image for post
Image for post

That’s it! This resets the cache and the next time a user hits that region’s endpoint, they receive the updated content. Note that, in order to test that this worked in different regions, I used Bitdefender VPN to hide my computer’s IP address and hit the cloudfront endpoint from multiple geographic locations. Also note that, every time I make an update to index.html, I will have to create another invalidation if I want to avoid that 24-hour lag-time. More about how to solve this problem is here.

Congratulations — you’ve successfully built a password-protected website on AWS using CloudFront and Lambda@Edge!

Note: Nothing in this blog post is original! All of the code and ideas are taken from other authors more experienced than myself; I just rearranged their information in a way that worked for me. If you want to dive deep, go to these sources here:

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store