{"id":881,"date":"2022-01-06T14:03:00","date_gmt":"2022-01-06T20:03:00","guid":{"rendered":"https:\/\/www.joshualyman.com\/?p=881"},"modified":"2022-01-05T13:03:28","modified_gmt":"2022-01-05T19:03:28","slug":"add-http-basic-authentication-to-cloudfront-distributions","status":"publish","type":"post","link":"https:\/\/www.joshualyman.com\/2022\/01\/add-http-basic-authentication-to-cloudfront-distributions\/","title":{"rendered":"Add HTTP Basic Authentication to CloudFront Distributions"},"content":{"rendered":"\n

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.<\/p>\n\n\n\n

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.<\/p>\n\n\n\n

\"\"<\/a><\/figure><\/div>\n\n\n\n

There are several tutorials on the web for implementing this using Lambda@Edge functions, but not too long ago AWS introduced CloudFront Functions<\/a>, which run at the edge location itself as opposed to at the regional edge location (one layer deeper) for Lambda@Edge. Additionally, CloudFront Functions are \u2159 the price of Lambda@Edge executions, and as a bonus you get 2,000,000 invocations for free in the Free Tier<\/a>, 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 L@E implementation. You can do less with CF Functions than L@E, but enough for this particular problem. So on to the code!<\/p>\n\n\n\n

TOC:<\/h3>\n\n\n\n
  1. HTTP Basic Authentication Refresher<\/a><\/li>
  2. CloudFront Function Basic Auth Check Code<\/a><\/li>
  3. Hooking Up the Function<\/a><\/li><\/ol>\n\n\n\n

    HTTP Basic Authentication Refresher<\/h2>\n\n\n\n

    Spacing on the details of how HTTP Basic Auth works? Yep, me too. Luckily, it’s quite simple.<\/p>\n\n\n\n

    Like many authentication schemes in HTTP, credentials are passed in the Authorization<\/code> header of the HTTP request. In the Basic auth mode, credentials are simply a combo of [username]:[password]<\/code>, and base64-encoded, with “Basic<\/code>” prepended to indicate the challenge type. So if a user’s name was john<\/code> and his password was foobar<\/code>, the Authorization<\/code> header contents would look like this:<\/p>\n\n\n\n

    Basic am9objpmb29iYXI=<\/code><\/p>\n\n\n\n

    “Woah woah, base64 is not<\/em> 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??? \ud83d\ude42<\/p>\n\n\n\n

    \"\"<\/a>
    What a proper Authorization header looks like in dev tools.<\/figcaption><\/figure><\/div>\n\n\n\n

    On the server’s end, it will check for the existence of the Authorization<\/code> 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<\/code> header, then the server will respond with a 401 error and a WWW-Authenticate<\/code> header with the value set to Basic<\/code>, indicating that the browser must give the Basic auth challenge to the user before performing further requests. That WWW-Authenticate: Basic<\/code> 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.<\/p>\n\n\n\n

    \"\"<\/a>
    Inspecting the challenge header that comes back telling the browser to open the credentials dialog. The “realm” portion is helper text you can include for the user, but not all browsers will display it.<\/figcaption><\/figure><\/div>\n\n\n\n

    For a more detailed overview of HTTP authentication, along with some interesting additional options, see MDN’s HTTP Authentication page<\/a>.<\/p>\n\n\n\n

    CloudFront Function Basic Auth Check Code<\/h2>\n\n\n\n

    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. <\/p>\n\n\n\n

    We’ll check the original request object for the proper Authorization<\/code> 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<\/code> 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???<\/p>\n\n\n\n

    function<\/span> handler<\/span>(<\/span>event<\/span>)<\/span> {<\/span>\n  var<\/span> authHeaders<\/span> =<\/span> event<\/span>.<\/span>request<\/span>.<\/span>headers<\/span>.<\/span>authorization<\/span>;<\/span>\n\n  \/\/ The Base64-encoded Auth string that should be present.<\/span>\n  \/\/ It is an encoding of `Basic base64([username]:[password])`<\/span>\n  \/\/ The username and password are:<\/span>\n  \/\/      Username: john<\/span>\n  \/\/      Password: foobar<\/span>\n  var<\/span> expected<\/span> =<\/span> "Basic am9objpmb29iYXI="<\/span>;<\/span>\n\n  \/\/ If an Authorization header is supplied and it's an exact match, pass the<\/span>\n  \/\/ request on through to CF\/the origin without any modification.<\/span>\n  if<\/span> (<\/span>authHeaders<\/span> &&<\/span> authHeaders<\/span>.<\/span>value<\/span> ===<\/span> expected<\/span>)<\/span> {<\/span>\n    return<\/span> event<\/span>.<\/span>request<\/span>;<\/span>\n  }<\/span>\n\n  \/\/ But if we get here, we must either be missing the auth header or the<\/span>\n  \/\/ credentials failed to match what we expected.<\/span>\n  \/\/ Request the browser present the Basic Auth dialog.<\/span>\n  var<\/span> response<\/span> =<\/span> {<\/span>\n    statusCode<\/span>:<\/span> 401<\/span>,<\/span>\n    statusDescription<\/span>:<\/span> "Unauthorized"<\/span>,<\/span>\n    headers<\/span>:<\/span> {<\/span>\n      "www-authenticate"<\/span>:<\/span> {<\/span>\n        value<\/span>:<\/span> 'Basic realm="Enter credentials for this super secure site"'<\/span>,<\/span>\n      },<\/span>\n    },<\/span>\n  };<\/span>\n\n  return<\/span> response<\/span>;<\/span>\n}<\/span>\n<\/pre><\/div>\n\n\n\n\n

    Relatively straightforward, but let me address a few basics for context.<\/p>\n\n\n\n

    All CloudFront functions must be named handler<\/code>, and they must be written in JavaScript. (If you want to write them in Python as well you can opt for Lambda@Edge functions.) Interestingly, the JS that CF Functions support is ES5, plus a smattering of ES6-9 features. For example, you can use string template literals are supported, but notably const<\/code> and let<\/code> are not, so be ready to dive back with your old frenemy var<\/code>. 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.<\/p>\n\n\n\n

    The handler<\/code> function is passed an event<\/code> object which has, among a few other things, request<\/code> and response<\/code> properties<\/a> 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.<\/p>\n\n\n\n

    In this little function, we first grab what we hope is the Authorization<\/code> header, and we also prepare a hard-coded version of what we would expect the correct credentials to be (our expected<\/code> variable). Then we check if the authHeaders<\/code> 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. <\/p>\n\n\n\n

    In any other failure mode we prompt for credentials. We do that by constructing our own new response<\/code> object, indicating the proper status code and WWW-Authenticate<\/code> header, and return it from the function.<\/p>\n\n\n\n

    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.<\/p>\n\n\n\n

    Hooking Up the Function<\/h2>\n\n\n\n

    The AWS documentation for creating, testing, and associating the function with a CloudFront distribution<\/a> 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.<\/p>\n\n\n\n

    That being said, let me offer just a few tips that I uncovered as I went through the process.<\/p>\n\n\n\n