There are two version of this site, a production version that you are almost certainly reading right now, and a development version here. Up until recently the development version was wide open to the world, which was not much of a problem except that it could be crawled, archived and, most unfortunately, share my spelling mistakes and chaotic writing process with the world. This post is sharing how I put the development version behind HTTP Basic Auth using Cloudfront Functions.

A bit about this site

This website is built using a static site generator called Hugo, deployed using the Serverless Framework and hosted in AWS using S3 for storage, Cloudfront as a CDN and Cloudfront Functions. I am already using Cloudfront Functions to implement a URL prettifying functionality, you can read more about that and some of the pros/cons of Cloudfront Functions here.

What is HTTP Basic Auth?

HTTP Basic Auth is a specification for providing rudimentary authorisation of a client using an unencrypted string in the Authorization header of a request. Whilst the username/password are base64 encoded this a reversible process and unless used in conjunction with some form of encryption-in-transit like TLS/HTTPS is not secure at all as the credentials can be be read by any third party handling the request on its way to the destination.

What are Cloudfront Functions?

Cloudfront Functions, whilst not a successor to Lambda@Edge, are an evolution of them. Whereas Lambda@Edge allows fully fledged lambda functions to run within a regional edge cache (AWS region) closest to the request origin, Cloudfront Functions run right at the edge where the content is ideally going to be served from. In terms of Cloudfront architecture you cannot get closer to the user. There are some restrictions to Cloudfront Functions you have to bear in mind like having a limited runtime, execution time, memory allowance, package size and that they are javascript only with no network access.

HTTP Basic Auth with Cloudfront Functions

The Authorization header is expected to have a value conforming to the pattern of Basic username:password where username:password is base64 encoded. To use HTTP Basic Auth from the browser you are not responsible for putting this value together, any modern browser will do that for you, but you will need to be able to read and validate it in the Cloudfront Function.

Base64 decoding/encoding

Unfortunately there is no access to the Buffer Class in the Cloudfront Function runtime, which would normally be the go to method for encoding/decoding encoded strings. There is also no option to install third party node modules so hand cranking a solution to a problem like this is required. The process of base64 encoding a string is well documented but I had no desire to implement that myself. Instead I lifted and refactored the logic a bit from the base64.js module on Github and included it directly in my Cloudfront Function code.

function encodeToBase64(str) {
  var chars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  for (
    // initialize result and counter
    var block, charCode, idx = 0, map = chars, output = "";
    // if the next str index does not exist:
    //   change the mapping table to "="
    //   check if d has no fractional digits
    str.charAt(idx | 0) || ((map = "="), idx % 1);
    // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
    output += map.charAt(63 & (block >> (8 - (idx % 1) * 8)))
  ) {
    charCode = str.charCodeAt((idx += 3 / 4));
    if (charCode > 0xff) {
      throw new InvalidCharacterError(
        "'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."
      );
    }
    block = (block << 8) | charCode;
  }
  return output;
}

Validating the credentials

With base64 encoding out of the way the only operation left to implement was validating the value of the Authorization header contained the expected username and password. There are no environment variables or secrets management within Cloudfront Functions, this is likely because they are intended to run with minimal latency and next to no resources. There is also no network access so getting secrets from Parameter Store for example is not possible. More complex operations that require secret secrets can use Lambda@Edge instead that provides a fully fledged lambda runtime. In my case I am not relying on HTTP Basic Auth for mission critical security so I hardcode a username and password into the Cloudfront Function and not worry about it any further.

To validate the credentials the expected value of the Authorization header is constructed and compared to the actual header value:

    var user = "XXXXX";
    var pass = "YYYYY";

    var requiredBasicAuth = "Basic " + encodeToBase64(`${user}:${pass}`);
    var match = headers.authorization.value != requiredBasicAuth;

When they match the request can be returned from the Cloudfront Function and go on its merry way. When they don’t an Unauthorized response should be returned with a www-authenticate header set to inform the browser that some form of authentication is required:

    return {
      statusCode: 401,
      statusDescription: "Unauthorized",
      headers: {
        "www-authenticate": { value: "Basic" },
      },
    };

As Basic authentication is requested pretty much any browser should know how to handle this and take care of presenting the user with an ability to input a username/password combination.

Summary

Using HTTP Basic Auth is not necessarily a robust security scheme, in this use case of putting a simple obstacle in front of my development site I consider it a good enough effort. Validating the credentials provided in a Cloudfront Function is not too complex but did present some challenges as has been covered in this post. Cloudfront Functions are an ideal resource for this use case as long as hardcoding the credentials in the code fits your security stance. For this site, I am the only one with access to the source code and if the credentials were leaked somehow gaining access to the development version of my site gets you almost exclusive access to terrible spelling!

Get in contact

If you have comments, questions or better ways to do anything that I have discussed in this post then please get in contact.