Securing Static Websites with Basic HTTP Authentication and CloudFront
How to password protect your static website with basic HTTP authentication (RFC 7617)
Introduction
Have you ever needed to quickly add authentication to protect content or a static website? Basic HTTP Authentication can be a simple and effective way to secure your content. It doesn’t require a complex configuration, and works well in many scenarios. In this article, we’ll explore how to set up Basic HTTP Authentication for a website served through Amazon Simple Storage Service (S3) and Amazon CloudFront.
- Have the AWS CLI Configured
- Have the AWS CDK Configured and bootstrapped in the region of your choice.
- Have a static website hosted using Cloudfront and S3.
- If you don't know how to do that yet. Feel free to take a look at my other posts—SPA Infra on AWS (Hands-on) and Static Website on AWS (Theory)
What is Basic HTTP Authentication?
When I mention Basic HTTP Authentication, I’m not referring to more complex solutions like OAuth, JSON Web Tokens (JWT), or session-based authentication. Instead, Basic HTTP Authentication is a built-in browser mechanism, defined by RFC 7617, which provides a straightforward way to verify users by prompting for a username and password to access specific content.
How Does it Work?
Figure 1. Basic HTTP Authentication Workflow
The process typically follows this flow:
-
Initial Request: The client initiates a request to access protected content by sending an HTTP GET request. If the request lacks an Authorization header, CloudFront (or the server) detects this and denies access.
-
401 Unauthorized Response: The server responds with a 401 Unauthorized status, including a www-authenticate header. This header specifies Basic realm="Enter your credentials", prompting the client to provide credentials.
-
Credentials Prompt: The browser, upon receiving the 401 response with the www-authenticate header, prompts the user to enter a username and password.
-
Authorization Header with Credentials: After the user submits their credentials, the client constructs a new request with an Authorization header. This header contains the word "Basic" followed by a space and the Base64-encoded string of the format username:password.
-
Verification by CloudFront: The server (in this case, CloudFront) receives the request with the Authorization header, matches the Base64-encoded credentials, and verifies their validity.
-
Access Granted or Denied:
- If the credentials are valid, the server responds with HTTP/1.1 200 OK, granting access to the protected content.
- If the credentials are invalid, the server responds with another 401 Unauthorized status, prompting the client to re-enter correct credentials.
Although Basic HTTP Authentication credentials are encoded in Base64, they aren’t encrypted. To protect your credentials, always use HTTPS. If you’re using CloudFront, HTTPS is enforced automatically, as CloudFront distributions redirect HTTP requests to HTTPS.
Implementing Basic HTTP Authentication with CloudFront Edge Functions
Edge Functions
Edge functions are function handlers that are executed at edge locations before they reach the origin. There are currently two types of supported edge functions: Lambda@Edge functions and CloudFront functions.
For the use case of Basic HTTP Authentication, CloudFront Functions make perfect sense. They offer sub-millisecond execution times, making them faster than Lambda@Edge, and are also more cost-effective for this lightweight task. Since Basic HTTP Authentication typically involves simple header manipulation without complex logic.
CloudFront function workflow
When the viewer requests the content, the request is first directed to the edge location. If the content is cached in the edge location, it is served immediately. Otherwise, the edge location will forward the request to the origin (in this case, an S3 bucket). In our case, the CloudFront function will be executed at the edge location before the request is forwarded to the origin and the Basic HTTP Authentication logic will be executed.
CloudFront Function Handler
Now that we understand a bit more about edge functions and how they work, let's take a look at the code.
// basic-http-auth.js
function btoa(input) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
input = String(input);
var bitmap, a, b, c,
result = "", i = 0,
rest = input.length % 3;
for (; i < input.length;) {
if ((a = input.charCodeAt(i++)) > 255
|| (b = input.charCodeAt(i++)) > 255
|| (c = input.charCodeAt(i++)) > 255)
throw new TypeError("Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.");
bitmap = (a << 16) | (b << 8) | c;
result += b64.charAt(bitmap >> 18 & 63) + b64.charAt(bitmap >> 12 & 63)
+ b64.charAt(bitmap >> 6 & 63) + b64.charAt(bitmap & 63);
}
return rest ? result.slice(0, rest - 3) + "===".substring(rest) : result;
}
function handler(event) {
var authHeaders = event.request.headers.authorization;
var username = 'user';
var password = 'pass';
var expected = 'Basic ' + btoa(`${username}:${password}`);
if (authHeaders && authHeaders.value === expected) {
return event.request;
}
var response = {
statusCode: 401,
statusDescription: "Unauthorized",
headers: {
"www-authenticate": {
value: 'Basic realm="Enter your credentials"',
},
},
};
return response;
}
The btoa() function
The btoa() function in this code is a custom implementation of Base64 encoding. It converts a given string (composed of Latin1 characters only) into a Base64-encoded string.
The handler function
This handler function enforces Basic HTTP Authentication by verifying the credentials provided in the request’s Authorization header. It begins by extracting the Authorization header and defining the expected credentials (in this case, "user:pass") encoded in Base64 format. If the provided Authorization header matches this expected value, the function allows the request to pass through to CloudFront or the origin server without modification. However, if the Authorization header is missing or doesn’t match, the function returns a 401 Unauthorized response, prompting the browser to display a login dialog where the user can enter the correct credentials.
Attaching the CloudFront Function to the Distribution
Here's how you can attach the CloudFront Function to the distribution using the AWS CDK. The actual implementation as you can see is very straightforward, you simply define the function, provide it the path to the function code, and then attach it to the distribution.
// website-stack.tsx
const websiteBucket = new s3.Bucket(this, `WebsiteBucket`, {
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const authFunction = new cloudfront.Function(this, 'BasicHttpAuthFunc', {
code: cloudfront.FunctionCode.fromFilePath({
filePath: path.join(__dirname, 'basic-http-auth.js')
}),
});
new cloudfront.Distribution(this, `Distribution`, {
defaultBehavior: {
origin: cloudfrontOrigins
.S3BucketOrigin
.withOriginAccessControl(websiteBucket),
functionAssociations: [{
function: authFunction,
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
}],
viewerProtocolPolicy: cloudfront
.ViewerProtocolPolicy
.REDIRECT_TO_HTTPS,
},
defaultRootObject: 'index.html',
});
You can find the source code for this example in the GitHub repository.
Conclusion
Sometimes, the simplest solution is the ideal solution. Basic HTTP Authentication is a simple yet effective way to add a layer of protection to static sites hosted on S3 and delivered with CloudFront. By using CloudFront Functions, you get a fast, low-cost solution that’s perfect for handling lightweight tasks like this right at the edge. It’s a straightforward approach that covers your bases without adding unnecessary complexity.