Alternative 3D Solutions Tutorial


Privately Publish a Web Application Using AWS Amplify


20 minutes

Posted on: July 17, 2020

Learn Sumerian
Privately Publish a Web Application Using AWS Amplify

Your progress

Tags

amplify
cloudfront
lambda
cognito

In this tutorial you will learn about:

Learn how to privately publish a basic web application using AWS Amplify

In this tutorial, you will learn how to create a private hosting solution for a single-page web application using AWS Amplify. You will then modify AWS CloudFormation templates to enable custom AWS Lambda functions that will protect your application, and any content contained within, from unauthorized access. You will also walk through the process of allowing users access to your appliction through a managed Amazon Cognito user pool.

Prerequisites

Step 1: Enable CloudFront Hosting

If you are following on from the previous tutorial, the first step will be to reconfigure your application to host through CloudFront instead of Amazon S3. This will build your app into a production environment and allow you to add additional AWS resources to the hosting solution, like Lambda functions.

  1. Open your terminal at the root directory for your project and enter the following:

     amplify remove hosting
    
  2. Select S3AndCloudfront from the list (it should be the only option), then confirm this choice when prompted. This will remove the existing dev hosting category from your application.

  3. Re-add the hosting:

     amplify add hosting
    
  4. As before, select Amazon CloudFront and S3 for the plugin module, but this time choose PROD for the environment setup. Use the defaults for the remaining prompts.

     ? Select the plugin module to execute Amazon CloudFront and S3
     ? Select the environment setup: PROD (S3 with CloudFront using HTTPS)
    
  5. Push these changes:

     amplify push
    

Your application is now configured to use CloudFront for its publishing environment. In the following steps, you will add/edit CloudFormation templates to add security resources to the app.

Step 2: Modify the Amplify configuration

In this step, you will add a new custom “Edge” category to the Amplify configuration that uses CloudFront’s Lambda@Edge feature to create functions that will trigger when users attempt to access your application.

  1. From your project’s root directory, navigate to amplify/backend.

     cd amplify/backend
    
  2. Open the file backend-config.json in your text editor. Replace its contents with the following:

     {
         "edge": {
             "LambdaEdgeProtection": {
                 "service": "LambdaAtEdge",
                 "providerPlugin": "awscloudformation",
                 "dependsOn": [
                     {
                         "category": "hosting",
                         "resourceName": "S3AndCloudFront",
                         "attributes": [
                             "CloudFrontDomainName"
                         ]
                     }
                 ]
             }
         },
         "hosting": {
             "S3AndCloudFront": {
                 "service": "S3AndCloudFront",
                 "providerPlugin": "awscloudformation"
             }
         }
     }
    
  3. Now you will add the CloudFormation templates necessary to deploy the authentication Lambda functions to your account. While still in amplify/backend, create a new directory for the Edge category, and within that another directory called LambdaEdgeProtection.

     cd amplify/backend
     mkdir edge
     mkdir edge/LambdaEdgeProtection
     cd edge/LambdaEdgeProtection
    
  4. In the LambdaEdgeProtection directory, create a file called template.json. Paste in the following as its contents:

     {
         "AWSTemplateFormatVersion": "2010-09-09",
         "Description": "Stack containing authentication lambda functions",
         "Transform": "AWS::Serverless-2016-10-31",
         "Parameters": {
         "env": {
             "Type": "String"
         },
         "hostingS3AndCloudFrontCloudFrontDomainName": {
             "Type": "String"
         }
         },
         "Resources": {
         "LambdaEdgeProtection": {
             "Type": "AWS::Serverless::Application",
             "Properties": {
             "Location": {
                 "ApplicationId": "arn:aws:serverlessrepo:us-east-1:520945424137:applications/cloudfront-authorization-at-edge",
                 "SemanticVersion": "1.2.1"
             },
             "Parameters": {
                 "CreateCloudFrontDistribution": "false",
                 "AlternateDomainNames":  { "Ref" : "hostingS3AndCloudFrontCloudFrontDomainName" },
                 "HttpHeaders" : "{ \"Content-Security-Policy\": \"default-src 'none'; img-src 'self' data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' https:\/\/*.amazonaws.com https:\/\/*.amazoncognito.com\", \"Strict-Transport-Security\": \"max-age=31536000; includeSubdomains; preload\", \"Referrer-Policy\": \"same-origin\", \"X-XSS-Protection\": \"1; mode=block\", \"X-Frame-Options\": \"DENY\", \"X-Content-Type-Options\": \"nosniff\" }"
             }
             }
         }
         }
     }
    

    This template will instruct Amplify to deploy a serverless application to manage the Lambda functions you’ll be using to authenticate users.

  5. In the same directory, create a file called parameters.json. Paste in the following as its contents:

     {
         "hostingS3AndCloudFrontCloudFrontDomainName": { 
         "Fn::GetAtt": [
             "hostingS3AndCloudFront",
             "Outputs.CloudFrontDomainName"
         ]
         }
     }
    
  6. Update the environment, then push the changes.

     amplify env checkout dev
     amplify push
    

This push will set up a Cognito user pool that allows you to provide specific users with access to your site; after the user logs in, they will be automatically redirected to your hosted application. Next, you will need to update the CloudFront distribution to use the new Lambda functions.

Step 3: Update the CloudFront distribution

In this step, you will modify the CloudFront distribution that Amplify generated to run the Lambda@Edge functions that we added in the new category.

  1. In your project directory, open the file at amplify/backend/hosting/S3AndCloudFront/template.json

  2. Replace that file’s contents with the following:

     {
         "AWSTemplateFormatVersion": "2010-09-09",
         "Description": "Hosting resource stack creation using Amplify CLI",
         "Parameters": {
             "env": {
                 "Type": "String"
             },
             "bucketName": {
                 "Type": "String"
             },
             "lambdaEdgeProtectionStackName": {
                 "Type": "String"
             }
         },
         "Conditions": {
             "ShouldNotCreateEnvResources": {
                 "Fn::Equals": [ { "Ref": "env" }, "NONE" ]
             }
         },
         "Resources": {
             "S3Bucket": {
                 "Type": "AWS::S3::Bucket",
                 "DeletionPolicy": "Retain",
                 "Properties": {
                     "BucketName": {
                         "Fn::If": [
                             "ShouldNotCreateEnvResources",
                             {
                                 "Ref": "bucketName"
                             },
                             {
                                 "Fn::Join": [ "", [ 
                                     { "Ref": "bucketName" }, 
                                     "-", 
                                     { "Ref": "env" }
                                     ]
                                 ]
                             }
                         ]
                     },
                     "WebsiteConfiguration": {
                         "IndexDocument": "index.html",
                         "ErrorDocument": "index.html"
                     },
                     "CorsConfiguration": {
                         "CorsRules": [
                             {
                                 "AllowedHeaders": [
                                     "Authorization",
                                     "Content-Length"
                                 ],
                                 "AllowedMethods": [
                                     "GET"
                                 ],
                                 "AllowedOrigins": [
                                     "*"
                                 ],
                                 "MaxAge": 3000
                             }
                         ]
                     }
                 }
             },
             "PrivateBucketPolicy": {
                 "Type": "AWS::S3::BucketPolicy",
                 "DependsOn": "OriginAccessIdentity",
                 "Properties": {
                     "PolicyDocument": {
                         "Id": "MyPolicy",
                         "Version": "2012-10-17",
                         "Statement": [
                             {
                                 "Sid": "APIReadForGetBucketObjects",
                                 "Effect": "Allow",
                                 "Principal": {
                                     "CanonicalUser": {
                                         "Fn::GetAtt": [
                                             "OriginAccessIdentity",
                                             "S3CanonicalUserId"
                                         ]
                                     }
                                 },
                                 "Action": "s3:GetObject",
                                 "Resource": {
                                     "Fn::Join": [ "", [ "arn:aws:s3:::", { "Ref": "S3Bucket" }, "/*" ] ]
                                 }
                             }
                         ]
                     },
                     "Bucket": {
                         "Ref": "S3Bucket"
                     }
                 }
             },
             "OriginAccessIdentity": {
                 "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
                 "Properties": {
                     "CloudFrontOriginAccessIdentityConfig": {
                         "Comment": "CloudFrontOriginAccessIdentityConfig"
                     }
                 }
             },
             "CloudFrontDistribution": {
                 "Type": "AWS::CloudFront::Distribution",
                 "DependsOn": [
                     "S3Bucket",
                     "OriginAccessIdentity"
                 ],
                 "Properties": {
                     "DistributionConfig": {
                         "CacheBehaviors": [
                             {
                                 "PathPattern": "/parseauth",
                                 "Compress": true,
                                 "ForwardedValues": {
                                     "QueryString": true
                                 },
                                 "LambdaFunctionAssociations": [
                                     {
                                         "EventType": "viewer-request",
                                         "LambdaFunctionARN": { "Fn::ImportValue" : { 
                                             "Fn::Sub": "${lambdaEdgeProtectionStackName}-ParseAuthHandler" } } 
                                     }
                                 ],
                                 "TargetOriginId": "dummy-origin",
                                 "ViewerProtocolPolicy": "redirect-to-https"
                             },
                             {
                                 "PathPattern": "/refreshauth",
                                 "Compress": true,
                                 "ForwardedValues": {
                                     "QueryString": true
                                 },
                                 "LambdaFunctionAssociations": [
                                     {
                                         "EventType": "viewer-request",
                                         "LambdaFunctionARN": { "Fn::ImportValue" : { "Fn::Sub": "${lambdaEdgeProtectionStackName}-RefreshAuthHandler" } } 
                                     }
                                 ],
                                 "TargetOriginId": "dummy-origin",
                                 "ViewerProtocolPolicy": "redirect-to-https"
                             },
                             {
                                 "PathPattern": "/signout",
                                 "Compress": true,
                                 "ForwardedValues": {
                                     "QueryString": true
                                 },
                                 "LambdaFunctionAssociations": [
                                     {
                                         "EventType": "viewer-request",
                                         "LambdaFunctionARN": { "Fn::ImportValue" : { "Fn::Sub": "${lambdaEdgeProtectionStackName}-SignOutHandler" } }
                                     }
                                 ],
                                 "TargetOriginId": "dummy-origin",
                                 "ViewerProtocolPolicy": "redirect-to-https"
                             }
                         ],
                         "Origins": [
                             {
                                 "DomainName": "example.org",
                                 "Id": "dummy-origin",
                                 "CustomOriginConfig": {
                                     "OriginProtocolPolicy": "match-viewer"
                                 }
                             },
                             {
                                 "DomainName": {
                                     "Fn::GetAtt": [
                                         "S3Bucket",
                                         "DomainName"
                                     ]
                                 },
                                 "Id": "hostingS3Bucket",
                                 "S3OriginConfig": {
                                     "OriginAccessIdentity": {
                                         "Fn::Join": [
                                             "",
                                             [
                                                 "origin-access-identity/cloudfront/",
                                                 {
                                                     "Ref": "OriginAccessIdentity"
                                                 }
                                             ]
                                         ]
                                     }
                                 }
                             }
                         ],
                         "Enabled": "true",
                         "DefaultCacheBehavior": {
                             "LambdaFunctionAssociations": [
                                 {
                                     "EventType": "viewer-request",
                                     "LambdaFunctionARN": { "Fn::ImportValue" : { "Fn::Sub": "${lambdaEdgeProtectionStackName}-CheckAuthHandler" } } 
                                 },
                                 {
                                     "EventType": "origin-response",
                                     "LambdaFunctionARN": { "Fn::ImportValue" : { "Fn::Sub": "${lambdaEdgeProtectionStackName}-HttpHeadersHandler" } } 
                                 }
                             ],  
                             "TargetOriginId": "hostingS3Bucket",
                             "ForwardedValues": {
                                 "QueryString": "true"
                             },
                             "ViewerProtocolPolicy": "redirect-to-https",
                             "DefaultTTL": 0,
                             "MaxTTL": 0,
                             "MinTTL": 0,
                             "Compress": true
                         },
                         "DefaultRootObject": "index.html",
                         "CustomErrorResponses": [
                             {
                                 "ErrorCachingMinTTL": 300,
                                 "ErrorCode": 400,
                                 "ResponseCode": 200,
                                 "ResponsePagePath": "/"
                             },
                             {
                                 "ErrorCachingMinTTL": 300,
                                 "ErrorCode": 403,
                                 "ResponseCode": 200,
                                 "ResponsePagePath": "/"
                             },
                             {
                                 "ErrorCachingMinTTL": 300,
                                 "ErrorCode": 404,
                                 "ResponseCode": 200,
                                 "ResponsePagePath": "/"
                             }
                         ]
                     }
                 }
             }
         },
         "Outputs": {
             "Region": {
                 "Value": {
                     "Ref": "AWS::Region"
                 }
             },
             "HostingBucketName": {
                 "Description": "Hosting bucket name",
                 "Value": {
                     "Ref": "S3Bucket"
                 }
             },
             "WebsiteURL": {
                 "Value": {
                     "Fn::GetAtt": [
                         "S3Bucket",
                         "WebsiteURL"
                     ]
                 },
                 "Description": "URL for website hosted on S3"
             },
             "S3BucketSecureURL": {
                 "Value": {
                     "Fn::Join": [
                         "",
                         [
                             "https://",
                             {
                                 "Fn::GetAtt": [
                                     "S3Bucket",
                                     "DomainName"
                                 ]
                             }
                         ]
                     ]
                 },
                 "Description": "Name of S3 bucket to hold website content"
             },
             "CloudFrontDistributionID": {
                 "Value": {
                     "Ref": "CloudFrontDistribution"
                 }
             },
             "CloudFrontDomainName": {
                 "Value": {
                     "Fn::GetAtt": [
                         "CloudFrontDistribution",
                         "DomainName"
                     ]
                 }
             },
             "CloudFrontSecureURL": {
                 "Value": {
                     "Fn::Join": [
                         "",
                         [
                             "https://",
                             {
                                 "Fn::GetAtt": [
                                     "CloudFrontDistribution",
                                     "DomainName"
                                 ]
                             }
                         ]
                     ]
                 }
             },
             "CloudFrontOriginAccessIdentity": {
                 "Value": {
                     "Ref": "OriginAccessIdentity"
                 }
             }
         }
     }
    
  3. Before we deploy, we need to define a parameter called lambdaEdgeProtectionStackName by passing it the name of the stack that contains the Lambda functions we created with the last amplify push. Start by opening the parameters.json file in the current directory and adding the new parameter after bucketName:

     {
         "bucketName": ...,
         "lambdaEdgeProtectionStackName": "{Stack name here}"
     }
    
  4. Next, sign in to the AWS Management Console and navigate to the CloudFormation console. You will see a list of nested stacks that Amplify automatically created for you, prefaced with the Amplify project ID that you chose. Find the stack where the Description matches Protect downloads of your content hosted on CloudFront with Cognito authentication using Lambda@Edge.

    • You can copy the stack name from the CloudFormation Console view by expanding the Stack Name column. Alternatively, you can click on the stack name to load a view with information about that stack. Here, the stack name can be found as part of the navigation underneath the console banner: Cloudformation > Stacks > [Stack Name]
  5. Copy it into the parameters.json file. Now CloudFormation will be able to look up and reference the Lambda functions you just created when you pushed your CloudFront configuration changes.

  6. Finally, deploy all of your changes:

     amplify publish
    

Once deployed, the URL for for your application will display a login page prompting for a username and password. In the last step, you will add new users to give them access through this login.

Step 4: Add an authorized user for your application

In this final step, you will learn how to add a new user to your user pool to allow access to your web application.

  1. Return to the AWS Management Console and open the Amazon Cognito Console.

  2. From the landing page, select “Manage User Pools.”

  3. Find a user pool similar to us-east-1-amplify-ProjectName. Select it.

  4. Select “Users and Groups” under “General Settings” from the left menu bar.

  5. Click “Create User.”

  6. In the dialogue that pops up, add a username (which must be an email address) and, if desired, a temporary password for your new user.

  7. Uncheck the “Mark as verified” boxes underneath the email address and phone number fields, then click “Create User.”

  8. Navigate again to the URL where your application is hosted and log in with your username and temporary password (which you will be required to change after logging in).

You now know how to protect and manage access to your web application with AWS Amplify and Lambda@Edge.

Back to Tutorials

© 2019 Amazon Web Services, Inc or its affiliates. All rights reserved.