# 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"