Serverless: password protecting a static website in an AWS S3 bucket

Written by lmakarov | Published 2017/08/30
Tech Story Tags: serverless | aws | cloudfront | aws-lambda | s3

TLDRvia the TL;DR App

Basic HTTP Authentication for S3 and CloudFront with Lambda@Edge

I’ve been looking for months for a solution to add Basic HTTP Authentication to S3 buckets on Amazon. There are options involving pre-signed URLs (single object only), using a 3rd-party free or commercial service (privacy concerns), spinning up an EC2/Heroku/etc. with middleware to proxy requests (complicated and not serverless), using page redirects and bucket policies (not secure). They all have one issue in common — all are workarounds.

I’ve almost taken the route of spinning up an Apache or Nginx container on ECS to act as a proxy for S3. Then, I ran across a blog post about Lambda@Edge being generally available in the US East (N. Virginia) Region (us-east-1).

In a nutshell, Lambda@Edge allows you to attach AWS Lambda functions to CloudFront behaviors. CloudFront is Amazon’s CDN solution and can sit in front of an S3 bucket, providing low latency responses, high transfer speeds, support for custom domains with free SSL certificates from Amazon, and it integrates with other AWS services, now including Lambda.

I scanned through the use cases mentioned in the announcement, and things started to look very promising:

You can use it to:- Inspect cookies and rewrite URLs to perform A/B testing.- Send specific objects to your users based on the User-Agent header.- Implement access control by looking for specific headers before passing requests to the origin.- Add, drop, or modify headers to direct users to different cached objects.- Generate new HTTP responses.- Cleanly support legacy URLs.- Modify or condense headers or URLs to improve cache utilization.- Make HTTP requests to other Internet resources and use the results to customize responses.

Intercepting request headers and generating new HTTP responses is exactly what we need to implement Basic HTTP Authentication!

Basic HTTP Authentication overview

Let’s quickly look into how Basic HTTP Authentication works.

Basic HTTP Authentication flow diagram (src)

Under the hood, it’s a simple client-server handshake. If a client requests a protected resource and does not provide a valid auth string via the Authorization request header, the server replies with a 401 Unauthorized status and a WWW-Authenticate: Basic response header. This response triggers a username and password prompt in a browser.

Basic HTTP Auth prompt in Chrome

After the user enters the credentials, the browser creates a base64 encoded auth string and uses it in the Authorization request header for all subsequent requests to the same realm. Since the subsequent requests come with a valid auth string in the Authorization header, the server responds with the 200 OK code and serves the content as usual.

Here’s a pseudo-code for the auth string generation:

authString = "Basic base64('username:password')"

Technically speaking, the encryption method and authentication handshake used in Basic HTTP Auth isn’t any more secure than passing credentials in plain text. That is why you should be using HTTPS anywhere sensitive information is passed over a network.

Implementing Basic HTTP Authentication in a Lambda function

Here’s a Lambda function for you, which implements the Basic HTTP Auth handshake:

You attach the function to the Viewer Request event type in the CloudFront behavior settings. The username and password are hardcoded in the function as authUser and authPass respectively. Adjust as necessary.

Lambda functions attached to viewer response/request events are not allowed to make networks calls, thus it is not possible to store or validate the credentials outside of the function (e.g., in DynamoDB).

Update: This is not the case anymore. Lambda@Edge now officially supports network access for viewer request/response events. It is now totally possible to manage credentials in a DynamoDB table or use any external API/provider to authenticate users, as long as the network call takes less than 5s to complete.

Check the blog post I mentioned previously on how to attach a Lambda function to a CloudFront behavior. I will be writing a more detailed post about the whole chain setup, but you should already get the gist and be able to put the pieces together.

Update: Dave Shepard wrote a step-by-step tutorial and Dominic Dumrauf put together a CloudFormation template to kick-start the whole setup. Check them out.

Here’s an example static website in an S3 bucket, with Basic Auth password protection handled by CloudFront and Lamda@Edge. Serverless.

https://d3e48dxmnonmo7.cloudfront.net/

username: userpassword: pass

Update: a concern was brought up in comments regarding going around CloudFront and accessing resources in S3 directly.

Access to the origin S3 bucket is restricted to the CloudFront distribution only. Users cannot go around CloudFront and access resources in the bucket directly (even if they know the direct URL within the bucket).

This can be easily configured under Origin Settings in CloudFront, no need to write S3 bucket policies manually:

S3 bucket origin settings

I hope you enjoyed reading and found this article useful.Clap all 50 times if so! This helps others discover content on Medium.


Published by HackerNoon on 2017/08/30