I have a static site hosted in an S3 bucket (old 8mm family videos) that I wanted to make available to a limited number of folks, but I didn’t want to bother with worrying about doing any sort of federated authentication or limiting via IP or VPN access. A very simple yet effective means of doing this is to just use HTTP Basic Authentication, where the browser itself will prompt the visitor for a username and password and pass it to the server for authentication.
The site in the bucket is served by a CloudFront distribution. There’s no option in either S3 or CloudFront to enable HTTP Basic Auth, but luckily it can be done by writing a function that is executed on each request for an asset in the distribution.
There are several tutorials on the web for implementing this using [email protected] functions, but not too long ago AWS introduced CloudFront Functions, which run at the edge location itself as opposed to at the regional edge location (one layer deeper) for [email protected] Additionally, CloudFront Functions are ⅙ the price of [email protected] executions, and as a bonus you get 2,000,000 invocations for free in the Free Tier, hooray! (This site would probably only ever incur pennies in function costs anyway, but hey.) And frankly, implementing the CloudFront Function for this use case is easier IMO than the corresponding [email protected] implementation. You can do less with CF Functions than [email protected], but enough for this particular problem. So on to the code!
HTTP Basic Authentication Refresher
Spacing on the details of how HTTP Basic Auth works? Yep, me too. Luckily, it’s quite simple.
Like many authentication schemes in HTTP, credentials are passed in the
Authorization header of the HTTP request. In the Basic auth mode, credentials are simply a combo of
[username]:[password], and base64-encoded, with “
Basic” prepended to indicate the challenge type. So if a user’s name was
john and his password was
Authorization header contents would look like this:
“Woah woah, base64 is not encryption and passing credentials around like that is super sketchy!” You are correct, dear reader, which is why you absolutely must have any connections utilizing HTTP Basic Auth be sent over HTTPS, or you’ll be leaking credentials left and right. But if you’ve got your SSL/TLS setup all worked out you should be fine, and you’re probably not protecting state secrets if all you are opting for is Basic auth, right? RIGHT??? 🙂
On the server’s end, it will check for the existence of the
Authorization header, pull out the credentials after it decodes the base64 string, and compare it to known matches. If the client did not pass an
Authorization header, then the server will respond with a 401 error and a
WWW-Authenticate header with the value set to
Basic, indicating that the browser must give the Basic auth challenge to the user before performing further requests. That
WWW-Authenticate: Basic challenge is what causes the browser to pop up the username/password combo box to the user and submit future requests in that session with the same credentials.
For a more detailed overview of HTTP authentication, along with some interesting additional options, see MDN’s HTTP Authentication page.
CloudFront Function Basic Auth Check Code
So what kind of function are we going to write to teach CloudFront the nifty Basic auth trick? It’s quite simple, actually. CloudFront allows us to hook into either the original request or subsequent response portions of the pipeline, and modify or replace the HTTP request/response objects.
We’ll check the original request object for the proper
Authorization header, and validate it if we find it. If not, we’ll cut the request process short and send back the 401 /
WWW-Authenticate: Basic challenge to get credentials from the user. The credentials in this case are hard-coded, but again hopefully you’re using this in a light security needs scenario. RIGHT???
Relatively straightforward, but let me address a few basics for context.
All CloudFront functions must be named
let are not, so be ready to dive back with your old frenemy
var. Also, CF Functions are limited to a 1 ms run time, so make sure you’re being conscientious about the performance of the code you write.
handler function is passed an
event object which has, among a few other things,
response properties with corresponding details. We can examine these properties and modify them as appropriate. When intercepting an original request, you can modify the request object and then return the request object to allow it to continue. Or you can return the response object instead to short-circuit the original request/response and not hit your origin. We do both in the code above. I’m not sure, but I’m guessing that the CF Function engine is looking at the shape of the returned object to determine if you returned it a request or response.
In this little function, we first grab what we hope is the
Authorization header, and we also prepare a hard-coded version of what we would expect the correct credentials to be (our
expected variable). Then we check if the
authHeaders variable is defined and if it exactly matches what we’re looking for. If it does then we pass along the original request unmodified because the user has shown they can access it.
In any other failure mode we prompt for credentials. We do that by constructing our own new
response object, indicating the proper status code and
WWW-Authenticate header, and return it from the function.
Now you could definitely code golf this particular little function if you wanted to, but hopefully this version of the code is readable and easy to understand.
Hooking Up the Function
The AWS documentation for creating, testing, and associating the function with a CloudFront distribution is solid. If using the AWS console, you go to “Functions” in the left sidebar of the CloudFront pages, create a new function, and copy and paste your code in. You can save changes to the code, test it in the same console page, and publish it when you’re happy. Then you go back to your CloudFront distribution to associate the published function with the distribution and event type (either “viewer request” or “viewer response”). If you read the docs it’ll walk you through it step by step.
That being said, let me offer just a few tips that I uncovered as I went through the process.
- All references to headers in the JS code must be lower-cased. The docs mention this, but I missed it the first time and couldn’t figure out why my code was breaking. That’s why you see
'www-authenticate'in the code above for the response instead of the proper casing.
- You can still use
console.log()in your code to test inside of the CloudFront console online. Useful for simple debugging.
- You can only associate one CloudFront Function with any one distribution and event type. So if you want to do two things when a request comes in, you have to put both actions into one function, instead of creating two functions and associating them both with a request in a distribution. (I needed to add
/index.htmlto a few requests, and ended up doing that in my same auth function, but not till after I had written, tested, and published it as a separate function.)
There you have it! After hooking up your function to your distribution (and sometimes waiting a few minutes for a cache invalidation), you should now have a pretty straightforward credentials dialog pop up whenever you go to access your site. Cheaper and slightly faster than [email protected], though less powerful, I found this a perfect little point solution to a particular problem. Hopefully this helps you out too.