CloudFront Function for basic auth, redirect, and serving from S3
Tags: aws, cloudfront, infrastructure
Published: 2023-03-15 11:24:44

---
title: CloudFront Function for basic auth, redirect, and serving from S3
published: true
date: 2023-03-15 14:08:28 UTC
tags: aws, cloudfront, infrastructure
canonical_url: https://stevenwoodson.com/blog/cloudfront-function-for-basic-auth-redirect-and-serving-from-s3
---

 ![](https://content.walnutcreekcreative.com/stevewoodson/wp-content/uploads/sites/3/2023/03/CloudFront-Function-for-basic-auth-redirect-and-serving-from-S3.png)

I recently had the opportunity to rethink how some CDN customizations were happening within an Amazon AWS managed project. Previously, we had used a couple Lambda@Edge functions to perform some light modifications during the **Viewer request** and **Origin request** cache behaviors. We had largely inherited the setup so hitting the reset button with some needed changes seemed a good idea.

## Initial CloudFront Function Research

Enter [CloudFront Functions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html), I had actually not heard of them previously but apparently they’ve been around [since 2021](https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/). After getting up to speed reading up on the use case it seemed an ideal evolution from Lambda@Edge. The parts I was most excited about was reading the following:

> offers submillisecond startup times, scales immediately to handle millions of requests per second, and is highly secure

Our use case needed the following:

<aside>
<p>A huge thank you to Joshua Lyman for writing up <a href="https://www.joshualyman.com/2022/01/add-http-basic-authentication-to-cloudfront-distributions/">Add HTTP Basic Authentication to CloudFront Distributions</a>, it ended up being a great head start to basic authentication I needed (item #1 on the list).</p>
</aside>
1. **Basic Authentication for all lower environments** , because we don’t want public access to content that’s still a work in progress
2. **More control over redirects** , because we’re using a JavaScript framework we weren’t able to control the response status as well as we would have liked (301 instead of 302 for example)
3. **Add index.html to request URLs that don’t include a file name** , also because we’re using a statically generated site based on a JavaScript framework.

### Gotchas

I was initially surprised – coming from the Lambda functions mentality – that I wasn’t able to use a couple of the JavaScript niceties like the Nullish coalescing operator (??), Optional chaining operator (?.), and Conditional (ternary) operator. Found out that that’s because [CloudFront functions only support ES 5.1](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html#writing-functions-javascript-features-restricted-features).

I was also pleasantly surprised to see a high focus on optimization and utilization, you even get a handy “[Compute Utilization](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/test-function.html?icmpid=docs_cf_help_panel#compute-utilization)” score every time you test so you can keep tabs on ensuring it’s comfortably within the max allowed time.

While much of these weren’t an issue for me, I also found out that:

- Dynamic code evaluation (`eval` for example) and Timers are not supported.
- The maximum memory assigned to CloudFront Functions is 2MB
- CloudFront Functions only respond to Viewer triggers whereas Lambda@Edge can work with both Viewer and Origin triggers. In my case I was able to merge the code we were using in both situations without any issue, but your mileage may vary.
- CloudFront Functions only support JavaScript (ES 5.1 as noted above), Lambda@Edge supports Node.js and Python too.
- CloudFront Functions do not have access to the network or filesystem.
- CloudFront Functions can only manipulate HTTP headers.

### Benefits

- Overall cheaper, and there’s a free tier available too.
- Building & testing are so much faster because you can do so directly within CloudFront, no more saving, creating a version, attributing that to the CloudFront cache behavior and deploying to test!
- Speed and scalability are amazing, you can support 10,000,000 requests per \_second\_ or more compared to up to 10,000 requests per second per Region for Lambda@Edge.

If you’re interested in more notes to help you choose which way to go, this [Choosing between CloudFront Functions and Lambda@Edge](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-functions.html#edge-functions-choosing) section was really helpful for me.

## The Code

Let’s get to work!

Final source

```
var authRedirect = {
  statusCode: 401,
  statusDescription: "Unauthorized",
  headers: {
    "www-authenticate": {
      value: 'Basic realm="Enter credentials to access this secure site"',
    },
  },
};

/**
 * Basic Authentication
 *
 * On all lower environment (dev & qa) we need basic authentication so general
 * public cannot access these sites. Bypass on prod and if authentication is
 * already secured.
 *
 * @param authorization - Incoming request authorization value
 * @param host - Incoming request host value
 * @returns boolean - Whether this request is authenticated
 */
function authCheck(authorization, host) {
  var productionHosts = ["mywebsite.com", "www.mywebsite.com", "blog.mywebsite.com"];

  // The Base64-encoded Auth string `secretuser:secretpass` that should be present.
  var expected = "Basic c2VjcmV0dXNlcjpzZWNyZXRwYXNz";

  // If this is a production website, we do not want to force any authentication
  if (productionHosts.indexOf(host) > -1) {
    return true;
  }

  // If an Authorization header is supplied and it's an exact match
  if (authorization && authorization.value === expected) {
    return true;
  }

  //Not production, auth header is either missing or failed to match
  return false;
}

/**
 * Redirect Check
 *
 * @param host - Incoming request host value
 * @param requestURI - Incoming request URI
 * @returns object|false - Returns an object containing the redirect details or false if no redirect necessary
 */
function redirectCheck(host, requestURI) {
  redirectPaths = {
    // '[FROM]': {'Location': '[TO]', 'status': [301|302]}},
    "/old-page-1": { Location: "/new-page-1/", status: 301 },
    "/old-page-2": { Location: "/new-page-2/", status: 301 },
    "/old-page-3": { Location: "/new-page-3/", status: 302 },
  };

  if (Object.keys(redirectPaths).indexOf(requestURI) >= 0) {
    var redirectTo = redirectPaths[requestURI];
    return redirectTo;
  }

  return false;
}

/**
 * Redirect Response
 *
 * @param redirectTo - object containing the redirect location and status details
 * @returns object - response object with redirect details
 */
function redirectResponse(redirectTo) {
  return {
    statusCode: redirectTo.status,
    statusDescription: "Moved",
    headers: {
      location: { value: redirectTo.Location },
    },
  };
}

/**
 * URL Rewrite
 *
 * Appends the /index.html to requests that don’t include a file name or extension
 * in the URL. Useful for single page applications or statically generated websites
 * that are hosted in an Amazon S3 bucket.
 *
 * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example-function-add-index.html
 * @param uri - Incoming request URI
 * @returns
 */
function uriRewrite(uri) {
  // Check whether the URI is missing a file name.
  if (uri.endsWith("/")) {
    return (uri += "index.html");
  }
  // Check whether the URI is missing a file extension.
  else if (!uri.includes(".")) {
    return (uri += "/index.html");
  } else {
    return uri;
  }
}

/**
 * Primary Handler
 *
 * Checks for authentication, then redirect, then URL rewriting
 *
 * @param event
 * @returns
 */
function handler(event) {
  var authorization = event.request.headers.authorization;
  var host =
    event.request.headers.host && event.request.headers.host.value
      ? event.request.headers.host.value.toLowerCase()
      : "";
  var requestURI = event.request.uri.replace(/\/$/, "");

  if (authCheck(authorization, host) === false) {
    return authRedirect;
  }

  var redirect = redirectCheck(host, requestURI);
  if (redirect !== false) {
    return redirectResponse(redirect);
  }

  event.request.uri = uriRewrite(requestURI);

  return event.request;
}

```