CloudFront Basic Authorizer

Photo by Marcos Mayer on Unsplash

Categories

  • Serverless

The combination of S3, CloudFront and Route 53 on the AWS platform is absolutely amazing when it comes to setting up a static website which scales gracefully and is highly available. Furthermore, launching the infrastructure can be completely scripted and automated. This provides a fast and easy way to get started quickly.

However, combining these services has some limitations when it comes to the fulfilment of some requirements. For example, some customers require that access to the website must be protected via authentication. There are several reasons for this requirement:

  • The content is not finished yet and the general public is not allowed to access it prior to a specific launch date
  • The website is part of a preproduction stage and therefore access should be limited and must be prevented for general public
  • The hosted contents might simply not be intended for the general public

Out of the box neither CloudFront nor S3 provide means for restricting access to the website by asking for credentials.

With the introduction of Lambda@Edge at re:invent 2016 this changed. Now we are able to invoke Lambda functions in various stages in the request-response-cycle of a request to CloudFront. This opens up the possibility to extend CloudFront with an implementation of the basic authentication protocol pretty easily.

The implementation

With basic authentication the server expects that the client is providing credentials via the Authorization header. If this header is missing in the request the server is responding with the HTTP status code 401 Unauthorized and provides an appropriate WWW-Authenticate header. When a client, for example a browser, is getting this response it shows a dialog to the user asking for the credentials. Once the credentials are provided by the user they are encoded and send to the server as a token via the Authorization header. The server can then decode this token and match the credentials to stored values. If they match the content can be sent back to the caller. Otherwise the server is responding with 401 Unauthorized again.

This process can easily be implemented as a Lambda function:

exports.handler = async (event) => {
  const request = event.Records[0].cf.request;

  if (request.headers.authorization === undefined) {
    return UnauthorizedResponse;
  }

  const authorizationToken = request.headers.authorization[0].value;

  const credentials = decodeAuthToken(authorizationToken);
  if (credentials === null) {
    return UnauthorizedResponse;
  }

  const config = this.loadConfiguration();

  const username = credentials.username;
  const passwordHash = hashedPassword(credentials.password, config.password_salt);

  if (username !== config.username || passwordHash !== config.password_hash) {
    return UnauthorizedResponse;
  }

  return request;
};

The function needs to be configured with the actual credentials a user needs to authenticate with. The implementation follows several best practices to do this. First, the password is not stored as cleartext but it is hashed. This way, someone who gets knowledge of the hash value can still not authenticate because the original password is not known. Second, not only the password is hashed but it is salted. That means that the password is prepended with a random alphanumeric string before applying the hash algorithm. This makes rainbow table attacks pretty unlikely to succeed.

CloudFront Configuration

For invoking the Lambda function via CloudFront it is important to consider two things:

  1. The function needs to be deployt to the region us-east-1
  2. A new version must be published for this function

Next, the CloudFront distribution for the website needs to be configured to invoke the specific version of the Lambda function for incoming requests. This is configured via the Cache Behaviour setting of the CloudFront distribution. It can be configured to be called only for specific paths or for the whole website.

Costs

All the services involved in this solution - Route 53, S3, CloudFront and Lambda@Edge - are billed according to their actual usage. The costs for executing the Lambda function used for implementing the basic authorizer is combined by the number of invocations, the execution time and the amount of memory. It costs less than $1 for performing the authentication for 1.000.000 requests.

Conclusion

Basic authentication can be added pretty easily to CloudFront distributions using a simple Lambda@Edge function. This opens up the possibility to restrict access to static websites hosted with AWS S3. The code, related scripts and CloudFormation templates can be found in the GitHub repository cloudfront-basic-authorizer. With this it should be pretty easy and straight forward to install it on any CloudFront distribution.