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

Serverless is a free and open-source web framework for easy deployments in the cloud. It allows easily to create a deployment using CloudFront, but sometimes you might want to implement basic authentication for your deployed webapps 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.