Skip to content

Instantly share code, notes, and snippets.

@jeroenvollenbrock
Last active April 8, 2024 15:51
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save jeroenvollenbrock/94edbbc62adc986d6d6a9a3076e66f5b to your computer and use it in GitHub Desktop.
Save jeroenvollenbrock/94edbbc62adc986d6d6a9a3076e66f5b to your computer and use it in GitHub Desktop.
AWS-CloudFront-basic-auth
var USERS = {
protecteddir: [{
username: 'user',
password: 'pass',
}],
};
//Response when auth is not valid.
var response401 = {
statusCode: 401,
statusDescription: 'Unauthorized',
headers: {
'www-authenticate': {value:'Basic'},
},
};
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
function btoa(input) {
input = String(input);
var bitmap, a, b, c,
result = "", i = 0,
rest = input.length % 3; // To determine the final padding
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);
}
// If there's need of padding, replace the last 'A's with equal signs
return rest ? result.slice(0, rest - 3) + "===".substring(rest) : result;
}
function handler(event) {
var request = event.request;
var headers = request.headers;
var auth = request.headers.authorization && request.headers.authorization.value;
var project = request.uri.substring(1).split(/\.|\//)[0];
var users = USERS[project];
if(users) {
if(!auth || !auth.startsWith('Basic ')) {
return response401;
}
if(!users.find(function(user) {
// Construct the Basic Auth string
var authString = 'Basic ' + btoa(user.username + ':' + user.password);
return authString === auth;
})) {
return response401;
}
}
return request;
}
@huylv95
Copy link

huylv95 commented May 17, 2021

Hi there, have you checked it with cloudfront function on AWS? thank you :))

@spoit
Copy link

spoit commented May 18, 2021

Can confirm this works like a charm

@huylv95
Copy link

huylv95 commented May 19, 2021

Can confirm this works like a charm

I created the cloudfront funtion based on guide as below, but basic authen not work for me. Can you share with me how can you do it?
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-tutorial.html

@spoit
Copy link

spoit commented May 19, 2021

Sure! I modified it somewhat to fit our purposes, but this works:

Cloudformation stack for the function:

AWSTemplateFormatVersion: 2010-09-09
Description: This will create basic auth to protect multiple CDN's

Resources:
    cloudfrontAuth:
      Type: AWS::CloudFront::Function
      Properties:
        Name: !Sub ${AWS::StackName}-basicAuth
        AutoPublish: true
        FunctionConfig:
          Comment: !Sub ${AWS::StackName}-basicAuth
          Runtime: cloudfront-js-1.0
        FunctionCode: |
          // List of users.
          var users = [
            {
              username: 'thename',
              password: 'thepassword',
            }
          ];

          // List of IP (v4)
          var ip = [
            '1.2.3.4', // some IP addresses that get direct access
          ];

          // Response when auth is not valid.
          var response401 = {
              statusCode: 401,
              statusDescription: 'Unauthorized',
              headers: {
                  'www-authenticate': {value:'Basic'},
              },
          };

          // Because we cannot import.
          var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
          function btoa(input) {
                  input = String(input);
                  var bitmap, a, b, c,
                      result = "", i = 0,
                      rest = input.length % 3; // To determine the final padding

                  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);
                  }

                  // If there's need of padding, replace the last 'A's with equal signs
                  return rest ? result.slice(0, rest - 3) + "===".substring(rest) : result;
              }

          // Main loop
          function handler(event) {
              var request = event.request;

              // IP Access.
              if (ip.find(function(ipA) {
                return ipA === event.viewer.ip;
              })) {
                return request;
              }

              // User Access.
              var auth = request.headers.authorization && request.headers.authorization.value;

              if (!auth || !auth.startsWith('Basic ')) {
                return response401;
              }

              if (!users.find(function(user) {
                // Construct the Basic Auth string
                var authString = 'Basic ' + btoa(user.username + ':' + user.password);

                return authString === auth;
              })) {
                return response401;
              }

              return request;
          }

Outputs:
  cloudfrontAuthFunctionARN:
    Value: !GetAtt cloudfrontAuth.FunctionMetadata.FunctionARN
    Export:
      Name: !Sub ${AWS::StackName}-cloudfrontAuthFunctionARN

Then how I clicked it into a cloudfront distribution:

...
  DefaultCacheBehavior:
     ....
            FunctionAssociations:
              - EventType: viewer-request
                FunctionARN:
                  Fn::ImportValue: <ARN-OF-YOUR-FUNCTION>

@huylv95
Copy link

huylv95 commented May 19, 2021

Thank you so much for your support, It's working for me now !!!

@spoit
Copy link

spoit commented May 19, 2021

yw!

@henrik
Copy link

henrik commented Nov 18, 2021

Thanks for this!

I adapted it slightly because I wanted a single user for the whole site.

Coworker @aalin also simplified it a little.

We set it up per @lehuy2012's comment above: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-tutorial.html

Edited 2022-10-02 to handle ":" in passwords per comments below.

// Source: https://gist.github.com/jeroenvollenbrock/94edbbc62adc986d6d6a9a3076e66f5b

var USERNAME = 'myuser';
var PASSWORD = 'mypassword';

var response401 = {
  statusCode: 401,
  statusDescription: 'Unauthorized',
  headers: {
    'www-authenticate': {value:'Basic'},
  },
};

function validateBasicAuth(authHeader) {
  var match = authHeader.match(/^Basic (.+)$/);
  if (!match) return false;

  var credentials = String.bytesFrom(match[1], 'base64').split(':', 2);

  return credentials[0] === USERNAME && credentials[1] === PASSWORD;
}

function handler(event) {
  var request = event.request;
  var headers = request.headers;
  var auth = (headers.authorization && headers.authorization.value) || '';

  if (!validateBasicAuth(auth)) return response401;

  return request;
}

@rashidnhm
Copy link

@henrik does your example hold up if the password itself contains :?

@aalin
Copy link

aalin commented Sep 28, 2022

@rashidnhm: It seems like it doesn't.

It should only split the credentials into two parts. This should work with passwords containing :.

var credentials = String.bytesFrom(match[1], 'base64').split(':', 2);

@henrik
Copy link

henrik commented Oct 2, 2022

Thanks @rashidnhm – good catch! And thanks @aalin :)

I've edited my comment above to incorporate @aalin's fix for ease of copy-pasting.

And to make it explicit – we only need to handle ":" in passwords, not in usernames. They're not allowed in usernames.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment