AWS Cloudfront is a CDN service by Amazon which is used to efficiently host Single Page Applications inside of AWS from a huge distributed network from nodes that are closest to the user.

Serverless is a free and open-source web framework for easy deployments in the cloud. It allows easy to create a deployment using CloudFront, but sometimes you might want to implement basic authentication for your deployed web apps to prevent search engines look at your site under development or just keep it under private access.

Due to its nature, CloudFront serves your content from different servers all over the world. Only one way to implement the ability to ask a user for basic auth is to apply special "Edge" Lambdas, which are uploaded to every server.

This post shows the most simple and working solution for CloudFront basic Auth using Lambda@Edge.

Since lambda functions for Lambda@Edge should be deployed to us-east-1 region we recommend to upload all stack in us-east-1 – content anyway will be served from distributed servers which will be located closer to a user so you don't have to worry about ping times.

resources:
   Resources:
     StaticSite:
       Type: AWS::S3::Bucket
       Properties:
         AccessControl: PublicRead
         BucketName: mybucketname
         WebsiteConfiguration:
           IndexDocument: index.html
 
     SiteBucketPolicy:
       Type: AWS::S3::BucketPolicy
       Properties:
         Bucket:
           Ref: StaticSite
         PolicyDocument:
           Statement:
           - Effect: Allow
             Principal: "*"
             Action: s3:GetObject
             Resource:
               Fn::Join:
               - ""
               - - "arn:aws:s3:::"
                 - Ref: StaticSite
                 - "/*"
 
     CloudFrontDistribution:
       Type: AWS::CloudFront::Distribution
       Properties:
         DistributionConfig:
           Aliases:
             - "mydomain.com"
           Origins:
             - DomainName: mybucketname.s3-website.${self:provider.region}.amazonaws.com
               Id: WebApp
               CustomOriginConfig:
                 HTTPPort: '80'
                 HTTPSPort: '443'
                 OriginProtocolPolicy: http-only
           Enabled: true
           Comment: PLAY
           DefaultRootObject: index.html
           PriceClass: PriceClass_100
           DefaultCacheBehavior:
             AllowedMethods:
               - GET
               - HEAD
             TargetOriginId: WebApp
             ForwardedValues:
               Cookies:
                 Forward: none
               QueryString: true
             SmoothStreaming: false
             LambdaFunctionAssociations:
               - EventType: 'viewer-request'
                 LambdaFunctionARN: !Join ["" , [!GetAtt BasicAuthEdgeLambda.Arn, ":1"]]
             ViewerProtocolPolicy: redirect-to-https
           ViewerCertificate:
             MinimumProtocolVersion: TLSv1
             SslSupportMethod: sni-only
             AcmCertificateArn: ${self:custom.sslCertARN}
 
     EdgeLambdaRole:
       Type: 'AWS::IAM::Role'
       Properties:
         AssumeRolePolicyDocument:
           Version: '2012-10-17'
           Statement:
           - Action: 'sts:AssumeRole'
             Principal:
               Service:
               - lambda.amazonaws.com
               - edgelambda.amazonaws.com
               - replicator.lambda.amazonaws.com
             Effect: Allow
         Policies:
         - PolicyName: EdgePoliciesLambdaPolicy
           PolicyDocument:
             Version: '2012-10-17'
             Statement:
             - Action:
               - 'lambda:GetFunction'
               - 'lambda:EnableReplication*'
               - 'lambda:InvokeFunction'
               - 'iam:CreateServiceLinkedRole'
               - 'cloudfront:UpdateDistribution'
               - 'cloudfront:CreateDistribution'
               - 'logs:CreateLogGroup'
               - 'logs:CreateLogStream'
               - 'logs:PutLogEvents'
               - 'xray:PutTraceSegments'
               - 'xray:PutTelemetryRecords'
               Effect: Allow
               Resource: '*'
 
     BasicAuthEdgeLambda:
       Type: 'AWS::Lambda::Function'
       Properties:
         Handler: 'index.beforeOriginRequest'
         Role: !GetAtt EdgeLambdaRole.Arn
         Code:
           ZipFile: !Sub "
             'use strict';
             const path = require('path');
 
             exports.beforeOriginRequest = (event, context, callback) => {
               const authUser = 'admin';
               const authPass = 'letmein';
 
               const request = event.Records[0].cf.request;
               const headers = request.headers;
               const authString = 'Basic ' + new Buffer(authUser + ':' + authPass).toString('base64');
               if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
                   const body = 'Unauthorized';
                   callback(null, {
                     status: '401',
                     statusDescription: 'Unauthorized',
                     body: body,
                     headers: {
                       'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}],
                     }
                   });
               }
               callback(null, request);
             };
           "
         Runtime: 'nodejs12.x'
         Timeout: '5'
         TracingConfig:
           Mode: 'Active'
 
     BasicAuthEdgeLambdaVersion:
       Type: 'AWS::Lambda::Version'
       Properties:
         FunctionName:
           Ref: BasicAuthEdgeLambda
 

You can configure credentials in const authUser = 'admin'; const authPass = 'letmein';.

Keep it simple and keep it secure.