DEV Community

Steven Woodson
Steven Woodson

Posted on • Originally published at stevenwoodson.com on

CloudFront Function for basic auth, redirect, and serving from S3

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, I had actually not heard of them previously but apparently they’ve been around since 2021. 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:

A huge thank you to Joshua Lyman for writing up Add HTTP Basic Authentication to CloudFront Distributions, it ended up being a great head start to basic authentication I needed (item #1 on the list).

  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.

I was also pleasantly surprised to see a high focus on optimization and utilization, you even get a handy “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 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;
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)