Serverless: password protecting a static website in an AWS S3 bucket
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 a 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 actually works.
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 back with a
401 Unauthorized status and a
WWW-Authenticate: Basic response header. This response triggers a username and password prompt in a browser.
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 back with the
200 OK code and serves the content as usually.
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 plan 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
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).
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.
Here’s an example of a static website stored in a S3 bucket, served by CloudFront over HTTPS (default ClodFront domain) with Basic HTTP Authentication done via Lambda@Edge. Serverless.