# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > Protect downloads of your content hosted on CloudFront with Cognito authentication using Lambda@Edge Metadata: AWS::ServerlessRepo::Application: Name: cloudfront-authorization-at-edge Description: > Protect downloads of your content hosted on CloudFront with Cognito authentication using Lambda@Edge. Includes: S3 Bucket w. sample SPA, Cognito User Pool with hosted UI set up, Lambda@Edge functions to validate JWTs and handle OAuth2 redirects. Author: AWS Samples SpdxLicenseId: MIT-0 LicenseUrl: LICENSE ReadmeUrl: SERVERLESS-REPO.md Labels: [ "cognito", "lambda@edge", "cloudfront", "s3", "react", "angular", "vue", "amplify", ] HomePageUrl: https://github.com/aws-samples/cloudfront-authorization-at-edge SemanticVersion: 2.1.6 SourceCodeUrl: https://github.com/aws-samples/cloudfront-authorization-at-edge Parameters: EmailAddress: Type: String Description: The email address of the user that will be created in the Cognito User Pool. Leave empty to skip user creation. Default: "" RedirectPathSignIn: Type: String Description: The URL path that should handle the redirect from Cognito after sign-in Default: /parseauth RedirectPathSignOut: Type: String Description: The URL path that should handle the redirect from Cognito after sign-out Default: / RedirectPathAuthRefresh: Type: String Description: The URL path that should handle the JWT refresh request Default: /refreshauth SignOutUrl: Type: String Description: The URL path that you can visit to sign-out Default: /signout AlternateDomainNames: Type: CommaDelimitedList Description: > If you intend to use one or more custom domain names for the CloudFront distribution, please set that up yourself on the CloudFront distribution after deployment. If you provide those domain names now (comma-separated) the necessary Cognito configuration will already be done for you (unless you're bring your own User Pool from a different AWS account). Alternatively, update the Cognito configuration yourself after deployment: add sign in and sign out URLs for your custom domains to the user pool app client settings. Default: "" CookieSettings: Type: String Description: > The settings for the cookies holding e.g. the JWTs. To be provided as a JSON object, mapping cookie type to setting. Provide the setting for the particular cookie type as a string, e.g. "Path=/; Secure; HttpOnly; Max-Age=1800; SameSite=Lax". If left to null, a default setting will be used that should be suitable given the value of "EnableSPAMode" parameter. Default: >- { "idToken": null, "accessToken": null, "refreshToken": null, "nonce": null } OAuthScopes: Type: CommaDelimitedList Description: The OAuth scopes to request the User Pool to add to the access token JWT Default: "phone, email, profile, openid, aws.cognito.signin.user.admin" HttpHeaders: Type: String Description: The HTTP headers to set on all responses from CloudFront. To be provided as a JSON object Default: >- { "Content-Security-Policy": "default-src 'none'; img-src 'self'; script-src 'self' https://code.jquery.com https://stackpath.bootstrapcdn.com; style-src 'self' 'unsafe-inline' https://stackpath.bootstrapcdn.com; 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" } EnableSPAMode: Type: String Description: Set to 'false' to disable SPA-specific features (i.e. when deploying a static site that won't interact with logout/refresh) Default: "true" AllowedValues: - "true" - "false" CreateCloudFrontDistribution: Type: String Description: > Set to 'false' to skip the creation of a CloudFront distribution and associated resources, such as the private S3 bucket and the sample React app. This may be of use to you, if you just want to create the Lambda@Edge functions to use with your own CloudFront distribution. Default: "true" AllowedValues: - "true" - "false" CookieCompatibility: Type: String Description: > Specify whether naming of cookies should be compatible with AWS Amplify (default) or Amazon Elasticsearch Service. In case of the latter, turn off SPA mode too: set parameter EnableSPAMode to false Default: "amplify" AllowedValues: - "amplify" - "elasticsearch" AdditionalCookies: Type: String Description: 'Specify one or more additional cookies to set after successfull sign-in. Specify as a JSON object––mapping cookie names to values and settings: {"cookieName1": "cookieValue1; HttpOnly; Secure"}' Default: >- {} UserPoolArn: Type: String Description: "Specify the ARN of an existing user pool to use that one instead of creating a new one. If specified, then UserPoolClientId must also be specified. Also, the User Pool should have a domain configured" Default: "" UserPoolAuthDomain: Type: String Description: > The auth domain of the existing User Pool, whose ARN you specified as UserPoolArn, e.g. "my-domain.auth.<region>.amazoncognito.com". If you don't provide this value, it will be looked up for you automatically. This automatic lookup only works if the pre-existing User Pool is in the same AWS account as this stack, so if it's not, make sure to explictly specify the UserPoolAuthDomain. Default: "" UserPoolClientId: Type: String Description: > Specify the ID of an existing user pool client to use that one instead of creating a new one. If specified, then UserPoolArn must also be specified. Note: new callback URL's will be added to the pre-existing user pool client (but only if it's in the same AWS account as this stack) Default: "" UserPoolClientSecret: Type: String Description: > The client secret of the existing User Pool Client, whose ClientId you specified as UserPoolClientId. If you don't provide this value, it will be looked up for you automatically. This automatic lookup only works if the pre-existing User Pool Client is in the same AWS account as this stack, so if it's not, make sure to explictly specify the UserPoolClientSecret. Default: "" UserPoolGroupName: Type: String Description: > Specify a group that users have to be part of to view the private site. Use this to further protect your private site, in case you don't want every user in the User Pool to have access. The UserPoolGroup will be created if the UserPool is also created (that happens when no UserPoolArn is set). If the UserPoolGroup is created and the default user is also created (when you specify EmailAddress), that user will also be added to the group. Default: "" Version: Type: String Description: "Changing this parameter after initial deployment forces redeployment of Lambda@Edge functions" Default: "2.1.6" LogLevel: Type: String Description: "Use for development: setting to a value other than none turns on logging at that level. Warning! This will log sensitive data, use for development only" Default: "none" AllowedValues: - "none" - "info" - "warn" - "error" - "debug" PermissionsBoundaryPolicyArn: Description: ARN of a boundary policy if your organisation uses some for roles, optional. Type: String Default: "" RewritePathWithTrailingSlashToIndex: Description: Do you want to append "index.html" to paths that end with a slash ("/")? Type: String Default: "false" AllowedValues: - "true" - "false" WebACLId: Description: AWS WAF Web ACL Id, if any, to associate with this distribution Type: String Default: "" DefaultRootObject: Description: The object that you want CloudFront to request from your origin (for example, index.html) when a viewer requests the root URL for your distribution (http://www.example.com). Type: String Default: index.html S3OriginDomainName: Description: > The S3 origin you want to front with CloudFront. Specify the bucket's region specific hostname, i.e. <bucket-name>.s3.<region>.amazonaws.com, and (optionally) also specify the parameter OriginAccessIdentity. If you don't provide an origin, and "CreateCloudFrontDistribution" is set to "true" (the default), then an S3 bucket will be created for you. Type: String Default: "" CustomOriginDomainName: Description: > The custom origin you want to front with CloudFront. If using an existing S3 bucket (in non-website mode), don't use this parameter, specify parameter "S3OriginDomainName" instead. If you don't provide an origin, and "CreateCloudFrontDistribution" is set to "true" (the default), then an S3 bucket will be created for you. Type: String Default: "" CustomOriginHeaderName: Description: > The HTTP header name of the (secret) custom header that you want CloudFront to send to your custom origin. Also specify parameter "CustomOriginHeaderValue". Only of use if you are also specifying parameter "CustomOriginDomainName". Type: String Default: "" CustomOriginHeaderValue: Description: > The HTTP header value of the (secret) custom header that you want CloudFront to send to your custom origin. Also specify parameter "CustomOriginHeaderName". Only of use if you are also specifying parameter "CustomOriginDomainName". Type: String Default: "" OriginAccessIdentity: Description: The Origin Access Identity you want to associate with your S3 origin, e.g. 'ABCDEFGHIJKLMN'. Type: String Default: "" CloudFrontAccessLogsBucket: Description: The (pre-existing) Amazon S3 bucket to store CloudFront access logs in, for example, myawslogbucket.s3.amazonaws.com. Only of use if "CreateCloudFrontDistribution" is set to "true" (the default). Type: String Default: "" Conditions: ApplyPermissionsBoundary: !Not [!Equals [!Ref PermissionsBoundaryPolicyArn, ""]] CreateUser: !And - !Not [!Equals [!Ref EmailAddress, ""]] - !Equals [!Ref UserPoolArn, ""] - !Equals [!Ref UserPoolClientId, ""] CreateCloudFrontDistribution: !Equals [!Ref CreateCloudFrontDistribution, "true"] SPAMode: !Equals [!Ref EnableSPAMode, "true"] StaticSiteMode: !Equals [!Ref EnableSPAMode, "false"] CreateSampleStaticSite: !And - !Equals [!Ref EnableSPAMode, "false"] - !Equals [!Ref CreateCloudFrontDistribution, "true"] - !Equals [!Ref CustomOriginDomainName, ""] - !Equals [!Ref S3OriginDomainName, ""] CreateSampleReactApp: !And - !Equals [!Ref EnableSPAMode, "true"] - !Equals [!Ref CreateCloudFrontDistribution, "true"] - !Equals [!Ref CustomOriginDomainName, ""] - !Equals [!Ref S3OriginDomainName, ""] CreateUserPoolAndClient: !Equals [!Ref UserPoolArn, ""] CreateUserPoolGroup: !And - !Condition CreateUserPoolAndClient - !Not [!Equals [!Ref UserPoolGroupName, ""]] AttachUserToPoolGroup: !And - !Condition CreateUser - !Condition CreateUserPoolGroup NoExistingUserPoolProvidedOrExistingUserPoolIsInThisAccount: !Or - !Equals [!Ref UserPoolArn, ""] - !Equals - !Ref AWS::AccountId - !Select [4, !Split [":", !Sub "${UserPoolArn}::::otheraccount"]] # Appending :::::otheraccount is a trick to make the select work even if UserPoolArn is empty UpdateUserPoolClient: !And - !Or - !Equals [!Ref CreateCloudFrontDistribution, "true"] - !Not [!Equals [!Join ["", !Ref AlternateDomainNames], ""]] - !Condition NoExistingUserPoolProvidedOrExistingUserPoolIsInThisAccount RegionIsNotUsEast1: !Not [!Equals [!Ref AWS::Region, "us-east-1"]] RewritePathWithTrailingSlashToIndex: !Equals [!Ref RewritePathWithTrailingSlashToIndex, "true"] CreateS3Bucket: !And - !Equals [!Ref CreateCloudFrontDistribution, "true"] - !Equals [!Ref CustomOriginDomainName, ""] - !Equals [!Ref S3OriginDomainName, ""] CreateOriginAccessIdentity: !And - !Equals [!Ref CreateCloudFrontDistribution, "true"] - !Equals [!Ref CustomOriginDomainName, ""] - !Equals [!Ref S3OriginDomainName, ""] - !Equals [!Ref OriginAccessIdentity, ""] OriginAccessIdentityProvided: !Not [!Equals [!Ref OriginAccessIdentity, ""]] UseS3Origin: !And - !Equals [!Ref CreateCloudFrontDistribution, "true"] - !Equals [!Ref CustomOriginDomainName, ""] UseWAF: !Not [!Equals [!Ref WebACLId, ""]] DefaultRootObjectProvided: !Not [!Equals [!Ref DefaultRootObject, ""]] CloudFrontAccessLogsBucketProvided: !Not [!Equals [!Ref CloudFrontAccessLogsBucket, ""]] CustomOriginHeaderProvided: !And - !Not [!Equals [!Ref CustomOriginHeaderName, ""]] - !Not [!Equals [!Ref CustomOriginHeaderValue, ""]] LookupAuthDomain: !And - !Condition NoExistingUserPoolProvidedOrExistingUserPoolIsInThisAccount - !Equals [!Ref UserPoolAuthDomain, ""] LookupClientSecret: !And - !Condition StaticSiteMode - !Condition NoExistingUserPoolProvidedOrExistingUserPoolIsInThisAccount - !Equals [!Ref UserPoolClientSecret, ""] Globals: Function: Timeout: 60 PermissionsBoundary: !If - ApplyPermissionsBoundary - !Ref PermissionsBoundaryPolicyArn - !Ref AWS::NoValue Runtime: nodejs16.x Resources: S3Bucket: Type: AWS::S3::Bucket Condition: CreateS3Bucket Properties: AccessControl: Private BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 CheckAuthHandler: Type: AWS::Serverless::Function Properties: CodeUri: src/lambda-edge/check-auth/ Handler: bundle.handler Role: !GetAtt LambdaEdgeExecutionRole.Arn Timeout: 5 ParseAuthHandler: Type: AWS::Serverless::Function Properties: CodeUri: src/lambda-edge/parse-auth/ Handler: bundle.handler Role: !GetAtt LambdaEdgeExecutionRole.Arn Timeout: 5 RefreshAuthHandler: Type: AWS::Serverless::Function Properties: CodeUri: src/lambda-edge/refresh-auth/ Handler: bundle.handler Role: !GetAtt LambdaEdgeExecutionRole.Arn Timeout: 5 HttpHeadersHandler: Type: AWS::Serverless::Function Properties: CodeUri: src/lambda-edge/http-headers/ Handler: bundle.handler Role: !GetAtt LambdaEdgeExecutionRole.Arn Timeout: 5 SignOutHandler: Type: AWS::Serverless::Function Properties: CodeUri: src/lambda-edge/sign-out/ Handler: bundle.handler Role: !GetAtt LambdaEdgeExecutionRole.Arn Timeout: 5 TrailingSlashHandler: Type: AWS::Serverless::Function Condition: RewritePathWithTrailingSlashToIndex Properties: CodeUri: src/lambda-edge/rewrite-trailing-slash/ Handler: bundle.handler Role: !GetAtt LambdaEdgeExecutionRole.Arn Timeout: 5 LambdaEdgeExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - edgelambda.amazonaws.com - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" PermissionsBoundary: !If - ApplyPermissionsBoundary - !Ref PermissionsBoundaryPolicyArn - !Ref AWS::NoValue UsEast1Deployment: Type: Custom::UsEast1LambdaEdgeDeployment Condition: RegionIsNotUsEast1 Properties: ServiceToken: !GetAtt UsEast1DeploymentHandler.Arn LambdaRoleArn: !GetAtt LambdaEdgeExecutionRole.Arn CheckAuthHandlerArn: !GetAtt CheckAuthHandler.Arn ParseAuthHandlerArn: !GetAtt ParseAuthHandler.Arn RefreshAuthHandlerArn: !GetAtt RefreshAuthHandler.Arn HttpHeadersHandlerArn: !GetAtt HttpHeadersHandler.Arn SignOutHandlerArn: !GetAtt SignOutHandler.Arn TrailingSlashHandlerArn: !If - RewritePathWithTrailingSlashToIndex - !GetAtt TrailingSlashHandler.Arn - !Ref AWS::NoValue Version: !Ref Version UsEast1DeploymentHandler: Type: AWS::Serverless::Function Condition: RegionIsNotUsEast1 Properties: CodeUri: src/cfn-custom-resources/us-east-1-lambda-stack Handler: index.handler Timeout: 900 Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: - cloudformation:CreateChangeSet - cloudformation:CreateStack - cloudformation:DeleteChangeSet - cloudformation:DeleteStack - cloudformation:DescribeChangeSet - cloudformation:DescribeStacks - cloudformation:ExecuteChangeSet - cloudformation:UpdateStack Resource: - !Sub "arn:${AWS::Partition}:cloudformation:us-east-1:${AWS::AccountId}:stack/${AWS::StackName}/*" - !Sub "arn:${AWS::Partition}:cloudformation:us-east-1:${AWS::AccountId}:changeset/${AWS::StackName}/*" - Effect: Allow Action: - cloudformation:DescribeStacks - cloudformation:GetTemplate Resource: !Ref AWS::StackId - Effect: Allow Action: - s3:GetObject - s3:PutObject - s3:CreateBucket - s3:DeleteBucket Resource: !Sub "arn:${AWS::Partition}:s3:::*-authedgedeploymentbucket-*" - Effect: Allow Action: lambda:GetFunction Resource: - !GetAtt ParseAuthHandler.Arn - !GetAtt CheckAuthHandler.Arn - !GetAtt HttpHeadersHandler.Arn - !GetAtt RefreshAuthHandler.Arn - !GetAtt SignOutHandler.Arn - !If - RewritePathWithTrailingSlashToIndex - !GetAtt TrailingSlashHandler.Arn - !Ref AWS::NoValue - Effect: Allow Action: - lambda:GetFunction - lambda:CreateFunction - lambda:DeleteFunction - lambda:UpdateFunctionCode - lambda:UpdateFunctionConfiguration - lambda:TagResource - lambda:ListTags Resource: - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-CheckAuthHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-ParseAuthHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-RefreshAuthHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-SignOutHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-HttpHeadersHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-TrailingSlashHandler-*" - Effect: Allow Action: iam:PassRole Resource: !GetAtt LambdaEdgeExecutionRole.Arn CloudFrontDistribution: Type: AWS::CloudFront::Distribution Condition: CreateCloudFrontDistribution Properties: DistributionConfig: CacheBehaviors: - PathPattern: !Ref RedirectPathSignIn Compress: true ForwardedValues: QueryString: true LambdaFunctionAssociations: - EventType: viewer-request LambdaFunctionARN: !GetAtt ParseAuthHandlerCodeUpdate.FunctionArn TargetOriginId: dummy-origin ViewerProtocolPolicy: redirect-to-https - PathPattern: !Ref RedirectPathAuthRefresh Compress: true ForwardedValues: QueryString: true LambdaFunctionAssociations: - EventType: viewer-request LambdaFunctionARN: !GetAtt RefreshAuthHandlerCodeUpdate.FunctionArn TargetOriginId: dummy-origin ViewerProtocolPolicy: redirect-to-https - PathPattern: !Ref SignOutUrl Compress: true ForwardedValues: QueryString: true LambdaFunctionAssociations: - EventType: viewer-request LambdaFunctionARN: !GetAtt SignOutHandlerCodeUpdate.FunctionArn TargetOriginId: dummy-origin ViewerProtocolPolicy: redirect-to-https DefaultCacheBehavior: Compress: true ForwardedValues: QueryString: true LambdaFunctionAssociations: - EventType: viewer-request LambdaFunctionARN: !GetAtt CheckAuthHandlerCodeUpdate.FunctionArn - EventType: origin-response LambdaFunctionARN: !GetAtt HttpHeadersHandlerCodeUpdate.FunctionArn - !If - RewritePathWithTrailingSlashToIndex - EventType: origin-request LambdaFunctionARN: !GetAtt TrailingSlashHandlerCodeUpdate.FunctionArn - !Ref AWS::NoValue TargetOriginId: protected-origin ViewerProtocolPolicy: redirect-to-https Enabled: true DefaultRootObject: !If - DefaultRootObjectProvided - !Ref DefaultRootObject - !Ref AWS::NoValue WebACLId: !If - UseWAF - !Ref WebACLId - !Ref AWS::NoValue Logging: !If - CloudFrontAccessLogsBucketProvided - Bucket: !Ref CloudFrontAccessLogsBucket - !Ref AWS::NoValue Origins: - DomainName: !If - UseS3Origin - !If - CreateS3Bucket - !GetAtt S3Bucket.RegionalDomainName - !Ref S3OriginDomainName - !Ref CustomOriginDomainName Id: protected-origin CustomOriginConfig: !If - UseS3Origin - !Ref AWS::NoValue - OriginProtocolPolicy: https-only OriginCustomHeaders: !If - UseS3Origin - !Ref AWS::NoValue - !If - CustomOriginHeaderProvided - - HeaderName: !Ref CustomOriginHeaderName HeaderValue: !Ref CustomOriginHeaderValue - !Ref AWS::NoValue S3OriginConfig: !If - UseS3Origin - OriginAccessIdentity: !If - CreateOriginAccessIdentity - !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" - !If - OriginAccessIdentityProvided - !Sub "origin-access-identity/cloudfront/${OriginAccessIdentity}" - !Ref AWS::NoValue - !Ref AWS::NoValue - DomainName: will-never-be-reached.org Id: dummy-origin CustomOriginConfig: OriginProtocolPolicy: match-viewer CustomErrorResponses: - !If - SPAMode # In SPA mode, 404's from S3 should return index.html, to enable SPA routing - ErrorCode: 404 ResponseCode: 200 ResponsePagePath: /index.html - !Ref AWS::NoValue CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: CreateOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: "CloudFront OAI" CloudfrontBucketPolicy: Type: AWS::S3::BucketPolicy Condition: CreateS3Bucket Properties: Bucket: !Ref S3Bucket PolicyDocument: Version: "2012-10-17" Statement: - Action: - "s3:GetObject" Effect: "Allow" Resource: !Join ["/", [!GetAtt S3Bucket.Arn, "*"]] Principal: CanonicalUser: !If - CreateOriginAccessIdentity - !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId - !Ref OriginAccessIdentity - Action: - "s3:ListBucket" Effect: "Allow" Resource: !GetAtt S3Bucket.Arn Principal: CanonicalUser: !If - CreateOriginAccessIdentity - !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId - !Ref OriginAccessIdentity UserPool: Type: AWS::Cognito::UserPool Condition: CreateUserPoolAndClient Properties: AdminCreateUserConfig: AllowAdminCreateUserOnly: true UserPoolName: !Ref AWS::StackName UsernameAttributes: - email UserPoolGroup: Type: AWS::Cognito::UserPoolGroup Condition: CreateUserPoolGroup Properties: GroupName: !Ref UserPoolGroupName UserPoolId: !Ref UserPool User: Type: AWS::Cognito::UserPoolUser Condition: CreateUser Properties: Username: !Ref EmailAddress UserPoolId: !Ref UserPool UserPoolGroupAttachment: Type: AWS::Cognito::UserPoolUserToGroupAttachment Condition: AttachUserToPoolGroup Properties: GroupName: !Ref UserPoolGroupName Username: !Ref EmailAddress UserPoolId: !Ref UserPool UserPoolClient: Type: AWS::Cognito::UserPoolClient Condition: CreateUserPoolAndClient Properties: UserPoolId: !Ref UserPool PreventUserExistenceErrors: ENABLED GenerateSecret: !If - StaticSiteMode - true - false AllowedOAuthScopes: !Ref OAuthScopes AllowedOAuthFlowsUserPoolClient: true AllowedOAuthFlows: - code SupportedIdentityProviders: - COGNITO CallbackURLs: - https://example.com/will-be-replaced LogoutURLs: - https://example.com/will-be-replaced UserPoolDomain: Type: AWS::Cognito::UserPoolDomain Condition: CreateUserPoolAndClient Properties: Domain: !Sub - "auth-${StackIdSuffix}" - StackIdSuffix: !Select - 2 - !Split - "/" - !Ref AWS::StackId UserPoolId: !Ref UserPool UserPoolDomainLookup: Type: Custom::UserPoolDomainLookup Condition: LookupAuthDomain Properties: ServiceToken: !GetAtt UserPoolDomainLookupHandler.Arn UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn UserPoolDomainLookupHandler: Type: AWS::Serverless::Function Condition: LookupAuthDomain Properties: CodeUri: src/cfn-custom-resources/user-pool-domain/ Handler: index.handler Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: - cognito-idp:DescribeUserPool Resource: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn UserPoolClientUpdate: Type: Custom::UserPoolClientUpdate Condition: UpdateUserPoolClient Properties: ServiceToken: !GetAtt UserPoolClientUpdateHandler.Arn UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn UserPoolClientId: !If - CreateUserPoolAndClient - !Ref UserPoolClient - !Ref UserPoolClientId CloudFrontDistributionDomainName: !If - CreateCloudFrontDistribution - !GetAtt CloudFrontDistribution.DomainName - "" RedirectPathSignIn: !Ref RedirectPathSignIn RedirectPathSignOut: !Ref RedirectPathSignOut AlternateDomainNames: !Ref AlternateDomainNames OAuthScopes: !Ref OAuthScopes UserPoolClientUpdateHandler: Type: AWS::Serverless::Function Properties: CodeUri: src/cfn-custom-resources/user-pool-client/ Handler: index.handler Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: - cognito-idp:UpdateUserPoolClient - cognito-idp:DescribeUserPoolClient Resource: "*" # * to be able to clean up after myself, if you change the User Pool Client ID ClientSecretRetrieval: Type: Custom::ClientSecretRetrieval Condition: LookupClientSecret Properties: ServiceToken: !GetAtt ClientSecretRetrievalHandler.Arn UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn UserPoolClientId: !If - CreateUserPoolAndClient - !Ref UserPoolClient - !Ref UserPoolClientId ClientSecretRetrievalHandler: Type: AWS::Serverless::Function Condition: LookupClientSecret Properties: CodeUri: src/cfn-custom-resources/client-secret-retrieval/ Handler: index.handler Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: - cognito-idp:DescribeUserPoolClient Resource: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn CognitoJwksFetchHandler: Type: AWS::Serverless::Function Properties: CodeUri: src/cfn-custom-resources/fetch-jwks/ Handler: index.handler FetchedJwks: Type: Custom::FetchedJwks Properties: ServiceToken: !GetAtt CognitoJwksFetchHandler.Arn Version: !Ref Version UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn StaticSite: Type: Custom::StaticSite Condition: CreateSampleStaticSite Properties: ServiceToken: !GetAtt StaticSiteHandler.Arn BucketName: !Ref S3Bucket StaticSiteHandler: Type: AWS::Serverless::Function Condition: CreateSampleStaticSite Properties: CodeUri: src/cfn-custom-resources/static-site/ Handler: index.handler Timeout: 180 MemorySize: 2048 Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: - s3:ListBucket Resource: - !GetAtt S3Bucket.Arn - Effect: Allow Action: - s3:DeleteObject - s3:PutObject Resource: - !GetAtt S3Bucket.Arn - !Join ["/", [!GetAtt S3Bucket.Arn, "*"]] ReactApp: Type: Custom::ReactApp Condition: CreateSampleReactApp Properties: ServiceToken: !GetAtt ReactAppHandler.Arn BucketName: !Ref S3Bucket UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn ClientId: !If - CreateUserPoolAndClient - !Ref UserPoolClient - !Ref UserPoolClientId CognitoAuthDomain: !If - LookupAuthDomain - !Ref UserPoolDomainLookup - !Ref UserPoolAuthDomain RedirectPathSignIn: !Ref RedirectPathSignIn RedirectPathSignOut: !Ref RedirectPathSignOut OAuthScopes: !Join - "," - !Ref OAuthScopes SignOutUrl: !Ref SignOutUrl CookieSettings: !Ref CookieSettings Version: !Ref Version ReactAppHandler: Type: AWS::Serverless::Function Condition: CreateSampleReactApp Properties: CodeUri: src/cfn-custom-resources/react-app/ Handler: index.handler Timeout: 300 MemorySize: 3008 EphemeralStorage: Size: 2048 Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: - s3:ListBucket Resource: - !GetAtt S3Bucket.Arn - Effect: Allow Action: - s3:DeleteObject - s3:PutObject Resource: - !GetAtt S3Bucket.Arn - !Join ["/", [!GetAtt S3Bucket.Arn, "*"]] ParseAuthHandlerCodeUpdate: Type: Custom::LambdaCodeUpdate Properties: ServiceToken: !GetAtt LambdaCodeUpdateHandler.Arn LambdaFunction: !If - RegionIsNotUsEast1 - !GetAtt UsEast1Deployment.ParseAuthHandler - !GetAtt ParseAuthHandler.Arn Version: !Ref Version Configuration: Fn::Sub: - > { "userPoolArn": "${UserPoolArn}", "jwks": ${FetchedJwks.Jwks}, "clientId": "${ClientId}", "clientSecret": "${ClientSecret}", "oauthScopes": ${OAuthScopesJsonArray}, "cognitoAuthDomain": "${UserPoolDomain}", "redirectPathSignIn": "${RedirectPathSignIn}", "redirectPathSignOut": "${RedirectPathSignOut}", "signOutUrl": "${SignOutUrl}", "redirectPathAuthRefresh": "${RedirectPathAuthRefresh}", "cookieSettings": ${CookieSettings}, "mode": "${Mode}", "httpHeaders": ${HttpHeaders}, "logLevel": "${LogLevel}", "nonceSigningSecret": "${NonceSigningSecret}", "cookieCompatibility": "${CookieCompatibility}", "additionalCookies": ${AdditionalCookies}, "requiredGroup": "${UserPoolGroupName}" } - Mode: !If - SPAMode - "spaMode" - "staticSiteMode" UserPoolDomain: !If - LookupAuthDomain - !Ref UserPoolDomainLookup - !Ref UserPoolAuthDomain ClientSecret: !If - StaticSiteMode - !If - LookupClientSecret - !GetAtt ClientSecretRetrieval.ClientSecret - !Ref UserPoolClientSecret - "" UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn ClientId: !If - CreateUserPoolAndClient - !Ref UserPoolClient - !Ref UserPoolClientId OAuthScopesJsonArray: !Join - "" - - '["' - !Join - '", "' - !Ref OAuthScopes - '"]' CheckAuthHandlerCodeUpdate: Type: Custom::LambdaCodeUpdate Properties: ServiceToken: !GetAtt LambdaCodeUpdateHandler.Arn LambdaFunction: !If - RegionIsNotUsEast1 - !GetAtt UsEast1Deployment.CheckAuthHandler - !GetAtt CheckAuthHandler.Arn Version: !Ref Version Configuration: Fn::Sub: - > { "userPoolArn": "${UserPoolArn}", "jwks": ${FetchedJwks.Jwks}, "clientId": "${ClientId}", "clientSecret": "${ClientSecret}", "oauthScopes": ${OAuthScopesJsonArray}, "cognitoAuthDomain": "${UserPoolDomain}", "redirectPathSignIn": "${RedirectPathSignIn}", "redirectPathSignOut": "${RedirectPathSignOut}", "signOutUrl": "${SignOutUrl}", "redirectPathAuthRefresh": "${RedirectPathAuthRefresh}", "cookieSettings": ${CookieSettings}, "mode": "${Mode}", "httpHeaders": ${HttpHeaders}, "logLevel": "${LogLevel}", "nonceSigningSecret": "${NonceSigningSecret}", "cookieCompatibility": "${CookieCompatibility}", "additionalCookies": ${AdditionalCookies}, "requiredGroup": "${UserPoolGroupName}" } - Mode: !If - SPAMode - "spaMode" - "staticSiteMode" UserPoolDomain: !If - LookupAuthDomain - !Ref UserPoolDomainLookup - !Ref UserPoolAuthDomain ClientSecret: !If - StaticSiteMode - !If - LookupClientSecret - !GetAtt ClientSecretRetrieval.ClientSecret - !Ref UserPoolClientSecret - "" UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn ClientId: !If - CreateUserPoolAndClient - !Ref UserPoolClient - !Ref UserPoolClientId OAuthScopesJsonArray: !Join - "" - - '["' - !Join - '", "' - !Ref OAuthScopes - '"]' HttpHeadersHandlerCodeUpdate: Type: Custom::LambdaCodeUpdate Properties: ServiceToken: !GetAtt LambdaCodeUpdateHandler.Arn LambdaFunction: !If - RegionIsNotUsEast1 - !GetAtt UsEast1Deployment.HttpHeadersHandler - !GetAtt HttpHeadersHandler.Arn Version: !Ref Version Configuration: Fn::Sub: > { "httpHeaders": ${HttpHeaders}, "logLevel": "${LogLevel}" } TrailingSlashHandlerCodeUpdate: Type: Custom::LambdaCodeUpdate Condition: RewritePathWithTrailingSlashToIndex Properties: ServiceToken: !GetAtt LambdaCodeUpdateHandler.Arn LambdaFunction: !If - RegionIsNotUsEast1 - !GetAtt UsEast1Deployment.TrailingSlashHandler - !GetAtt TrailingSlashHandler.Arn Version: !Ref Version Configuration: Fn::Sub: > { "logLevel": "${LogLevel}" } RefreshAuthHandlerCodeUpdate: Type: Custom::LambdaCodeUpdate Properties: ServiceToken: !GetAtt LambdaCodeUpdateHandler.Arn LambdaFunction: !If - RegionIsNotUsEast1 - !GetAtt UsEast1Deployment.RefreshAuthHandler - !GetAtt RefreshAuthHandler.Arn Version: !Ref Version Configuration: Fn::Sub: - > { "userPoolArn": "${UserPoolArn}", "jwks": ${FetchedJwks.Jwks}, "clientId": "${ClientId}", "clientSecret": "${ClientSecret}", "oauthScopes": ${OAuthScopesJsonArray}, "cognitoAuthDomain": "${UserPoolDomain}", "redirectPathSignIn": "${RedirectPathSignIn}", "redirectPathSignOut": "${RedirectPathSignOut}", "signOutUrl": "${SignOutUrl}", "redirectPathAuthRefresh": "${RedirectPathAuthRefresh}", "cookieSettings": ${CookieSettings}, "mode": "${Mode}", "httpHeaders": ${HttpHeaders}, "logLevel": "${LogLevel}", "nonceSigningSecret": "${NonceSigningSecret}", "cookieCompatibility": "${CookieCompatibility}", "additionalCookies": ${AdditionalCookies}, "requiredGroup": "${UserPoolGroupName}" } - Mode: !If - SPAMode - "spaMode" - "staticSiteMode" UserPoolDomain: !If - LookupAuthDomain - !Ref UserPoolDomainLookup - !Ref UserPoolAuthDomain ClientSecret: !If - StaticSiteMode - !If - LookupClientSecret - !GetAtt ClientSecretRetrieval.ClientSecret - !Ref UserPoolClientSecret - "" UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn ClientId: !If - CreateUserPoolAndClient - !Ref UserPoolClient - !Ref UserPoolClientId OAuthScopesJsonArray: !Join - "" - - '["' - !Join - '", "' - !Ref OAuthScopes - '"]' SignOutHandlerCodeUpdate: Type: Custom::LambdaCodeUpdate Properties: ServiceToken: !GetAtt LambdaCodeUpdateHandler.Arn LambdaFunction: !If - RegionIsNotUsEast1 - !GetAtt UsEast1Deployment.SignOutHandler - !GetAtt SignOutHandler.Arn Version: !Ref Version Configuration: Fn::Sub: - > { "userPoolArn": "${UserPoolArn}", "jwks": ${FetchedJwks.Jwks}, "clientId": "${ClientId}", "clientSecret": "${ClientSecret}", "oauthScopes": ${OAuthScopesJsonArray}, "cognitoAuthDomain": "${UserPoolDomain}", "redirectPathSignIn": "${RedirectPathSignIn}", "redirectPathSignOut": "${RedirectPathSignOut}", "signOutUrl": "${SignOutUrl}", "redirectPathAuthRefresh": "${RedirectPathAuthRefresh}", "cookieSettings": ${CookieSettings}, "mode": "${Mode}", "httpHeaders": ${HttpHeaders}, "logLevel": "${LogLevel}", "nonceSigningSecret": "${NonceSigningSecret}", "cookieCompatibility": "${CookieCompatibility}", "additionalCookies": ${AdditionalCookies}, "requiredGroup": "${UserPoolGroupName}" } - Mode: !If - SPAMode - "spaMode" - "staticSiteMode" UserPoolDomain: !If - LookupAuthDomain - !Ref UserPoolDomainLookup - !Ref UserPoolAuthDomain ClientSecret: !If - StaticSiteMode - !If - LookupClientSecret - !GetAtt ClientSecretRetrieval.ClientSecret - !Ref UserPoolClientSecret - "" UserPoolArn: !If - CreateUserPoolAndClient - !GetAtt UserPool.Arn - !Ref UserPoolArn ClientId: !If - CreateUserPoolAndClient - !Ref UserPoolClient - !Ref UserPoolClientId OAuthScopesJsonArray: !Join - "" - - '["' - !Join - '", "' - !Ref OAuthScopes - '"]' LambdaCodeUpdateHandler: Type: AWS::Serverless::Function Properties: CodeUri: src/cfn-custom-resources/lambda-code-update/ Handler: index.handler Policies: - Version: "2012-10-17" Statement: - Effect: Allow Action: - lambda:GetFunction - lambda:UpdateFunctionCode Resource: - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-CheckAuthHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-ParseAuthHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-RefreshAuthHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-SignOutHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-HttpHeadersHandler-*" - !Sub "arn:${AWS::Partition}:lambda:us-east-1:${AWS::AccountId}:function:*-TrailingSlashHandler-*" NonceSigningSecret: Type: Custom::NonceSigningSecret Properties: ServiceToken: !GetAtt RandomValueGenerator.Arn Length: 16 Version: !Ref Version RandomValueGenerator: Type: AWS::Serverless::Function Properties: CodeUri: src/cfn-custom-resources/generate-secret/ Handler: index.handler Outputs: S3Bucket: Description: The S3 Bucket where the SPA (React, Angular, Vue, ...) is uploaded to Condition: CreateS3Bucket Value: !Ref S3Bucket Export: Name: !Sub "${AWS::StackName}-S3Bucket" WebsiteUrl: Description: URL of the CloudFront distribution that serves your SPA from S3 Condition: CreateCloudFrontDistribution Value: !Sub "https://${CloudFrontDistribution.DomainName}" Export: Name: !Sub "${AWS::StackName}-WebsiteUrl" CloudFrontDistribution: Description: ID of the CloudFront distribution that serves your SPA from S3 Condition: CreateCloudFrontDistribution Value: !Ref CloudFrontDistribution Export: Name: !Sub "${AWS::StackName}-CloudFrontDistribution" UserPoolId: Description: The ID of the Cognito User Pool Condition: CreateUserPoolAndClient Value: !Ref UserPool Export: Name: !Sub "${AWS::StackName}-UserPoolId" ClientId: Description: Client ID to use to interact with the User Pool Condition: CreateUserPoolAndClient Value: !Ref UserPoolClient Export: Name: !Sub "${AWS::StackName}-ClientId" ClientSecret: Description: The client secret associated with the User Pool Client. This will be empty in SPA mode. Condition: StaticSiteMode Value: !If - LookupClientSecret - !GetAtt ClientSecretRetrieval.ClientSecret - !Ref UserPoolClientSecret Export: Name: !Sub "${AWS::StackName}-ClientSecret" CognitoAuthDomain: Description: The domain where the Cognito Hosted UI is served Condition: CreateUserPoolAndClient Value: !If - LookupAuthDomain - !Ref UserPoolDomainLookup - !Ref UserPoolAuthDomain Export: Name: !Sub "${AWS::StackName}-CognitoAuthDomain" RedirectUrisSignIn: Description: The URI(s) that will handle the redirect from Cognito after successfull sign-in Condition: UpdateUserPoolClient Value: !GetAtt UserPoolClientUpdate.RedirectUrisSignIn Export: Name: !Sub "${AWS::StackName}-RedirectUrisSignIn" RedirectUrisSignOut: Description: The URI(s) that will handle the redirect from Cognito after successfull sign-out Condition: UpdateUserPoolClient Value: !GetAtt UserPoolClientUpdate.RedirectUrisSignOut Export: Name: !Sub "${AWS::StackName}-RedirectUrisSignOut" ParseAuthHandler: Description: The Lambda function ARN to use in Lambda@Edge for parsing the URL of the redirect from the Cognito hosted UI after succesful sign-in Value: !GetAtt ParseAuthHandlerCodeUpdate.FunctionArn Export: Name: !Sub "${AWS::StackName}-ParseAuthHandler" CheckAuthHandler: Description: The Lambda function ARN to use in Lambda@Edge for checking the presence of a valid JWT Value: !GetAtt CheckAuthHandlerCodeUpdate.FunctionArn Export: Name: !Sub "${AWS::StackName}-CheckAuthHandler" HttpHeadersHandler: Description: The Lambda function ARN to use in Lambda@Edge for setting HTTP security headers Value: !GetAtt HttpHeadersHandlerCodeUpdate.FunctionArn Export: Name: !Sub "${AWS::StackName}-HttpHeadersHandler" RefreshAuthHandler: Description: The Lambda function ARN to use in Lambda@Edge for getting new JWTs using the refresh token Value: !GetAtt RefreshAuthHandlerCodeUpdate.FunctionArn Export: Name: !Sub "${AWS::StackName}-RefreshAuthHandler" SignOutHandler: Description: The Lambda function ARN to use in Lambda@Edge for signing out Value: !GetAtt SignOutHandlerCodeUpdate.FunctionArn Export: Name: !Sub "${AWS::StackName}-SignOutHandler" TrailingSlashHandler: Description: The Lambda function ARN to use in Lambda@Edge for appending index.html to paths ending with a slash ("/") Condition: RewritePathWithTrailingSlashToIndex Value: !GetAtt TrailingSlashHandlerCodeUpdate.FunctionArn Export: Name: !Sub "${AWS::StackName}-TrailingSlashHandler" CodeUpdateHandler: Description: The Lambda function ARN of the custom resource that adds configuration to a function and publishes a version for use as Lambda@Edge Value: !GetAtt LambdaCodeUpdateHandler.Arn Export: Name: !Sub "${AWS::StackName}-CodeUpdateHandler" UserPoolClientUpdateHandler: Description: The Lambda function ARN of the custom resource that updates the user pool client with the right redirect URI's for sign-in and sign-out Value: !GetAtt UserPoolClientUpdateHandler.Arn Export: Name: !Sub "${AWS::StackName}-UserPoolClientUpdateHandler"