##
## This template creates a simple Lambda that's fronted by an ALB with Cognito Hosted UI for authorization.
##

AWSTemplateFormatVersion:               "2010-09-09"
Description:                            "Example Lambda webservice with authentication"

Parameters:

  VpcId:
    Description:                        "VPC where the environment will be deployed"
    Type:                               "AWS::EC2::VPC::Id"

  PublicSubnets:
    Description:                        "Public subnets used for ELB"
    Type:                               "List<AWS::EC2::Subnet::Id>"

  DNSDomain:
    Description:                        "Route53-managed domain in which to create host entry (empty to ignore)"
    Type:                               "String"

  Hostname:
    Description:                        "Hostname to use as alias for load balancer (empty to ignore)"
    Type:                               "String"

  ACMCertificateArn:
    Description:                        "The ARN of an ACM-managed certificate that's valid for the specified hostname (empty to disable HTTPS)"
    Type:                               "String"


Resources:

##
## Lambda
##

  LambdaLogGroup:
    Type:                               "AWS::Logs::LogGroup"
    DeletionPolicy:                     "Delete"
    Properties:
      LogGroupName:                     !Sub "/aws/lambda/${AWS::StackName}"
      RetentionInDays:                  7

  LambdaRole:
    Type:                               "AWS::IAM::Role"
    Properties:
      Path:                             "/lambda/"
      RoleName:                         !Sub "${AWS::StackName}-ExecutionRole"
      AssumeRolePolicyDocument:
        Version:                        "2012-10-17"
        Statement:
          Effect:                       "Allow"
          Principal:
            Service:                    "lambda.amazonaws.com"
          Action:                       "sts:AssumeRole"
      ManagedPolicyArns:
        -                               "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"


  LambdaFunction:
    Type:                               "AWS::Lambda::Function"
    Properties: 
      FunctionName:                     !Sub "${AWS::StackName}"
      Description:                      "A simple function that responds to a load balancer request"
      Role:                             !GetAtt LambdaRole.Arn
      Runtime:                          "python3.7"
      Handler:                          "index.lambda_handler"
      Code: 
        ZipFile:                        |
                                        import json

                                        def lambda_handler(event, context):
                                            print(json.dumps(event))
                                            return {
                                                'statusCode': 200,
                                                'headers': {
                                                    'Content-Type': 'text/html'
                                                },
                                                'body': "Example data"
                                            }
      MemorySize:                       128
      Timeout:                          10


  InvocationPermission:
    Type:                               "AWS::Lambda::Permission"
    Properties: 
      Action:                           "lambda:InvokeFunction"
      FunctionName:                     !Ref LambdaFunction
      Principal:                        "elasticloadbalancing.amazonaws.com"


##
## Cognito User Pool
##

  CognitoUserPool:
    Type:                               "AWS::Cognito::UserPool"
    Properties:
      UserPoolName:                     !Sub "${AWS::StackName}"
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly:       false
        UnusedAccountValidityDays:      7
      MfaConfiguration:                 "OFF"
      Policies:
        PasswordPolicy:
          MinimumLength:                8
          RequireUppercase:             true
          RequireLowercase:             true
          RequireNumbers:               true
          RequireSymbols:               false
      Schema:
        - Name:                         "email"
          Required:                     true
          Mutable:                      true
          DeveloperOnlyAttribute:       false
          AttributeDataType:            "String"
          StringAttributeConstraints:
            MinLength:                  0
            MaxLength:                  2048
        - Name:                         "email_verified"
          Required:                     false
          Mutable:                      true
          DeveloperOnlyAttribute:       false
          AttributeDataType:            "Boolean"
      UsernameAttributes:               [ "email" ]
      AutoVerifiedAttributes:           [ "email" ]


  CognitoUserPoolDomain:
    Type:                               "AWS::Cognito::UserPoolDomain"
    Properties: 
      UserPoolId:                       !Ref CognitoUserPool
      Domain:                           !Sub "${Hostname}-auth"


  CognitoUserPoolClient:
    Type:                               "AWS::Cognito::UserPoolClient"
    Properties:
      UserPoolId:                       !Ref CognitoUserPool
      ClientName:                       "default"
      SupportedIdentityProviders:       [ "COGNITO" ]
      AllowedOAuthFlowsUserPoolClient:  true
      AllowedOAuthScopes:               [ "openid", "email" ]
      AllowedOAuthFlows:                [ "code" ]
      CallbackURLs:                     [ !Sub "https://${Hostname}.${DNSDomain}/oauth2/idpresponse" ]
      GenerateSecret:                   true
      RefreshTokenValidity:             7


##
## Load Balancer
##

  LoadBalancerSecurityGroup:
    Type:                               "AWS::EC2::SecurityGroup"
    Properties:
      GroupName:                        !Sub "${AWS::StackName}-LoadBalancerSecurityGroup"
      GroupDescription:                 "Allows ingress to the elastic load balancer"
      VpcId:                            !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol:                   "tcp"
          CidrIp:                       "0.0.0.0/0"
          FromPort:                     443
          ToPort:                       443


  LoadBalancerHostname:
    Type:                               "AWS::Route53::RecordSet"
    Properties:
      Name:                             !Sub "${Hostname}.${DNSDomain}"
      Type:                             "A"
      HostedZoneName:                   !Sub "${DNSDomain}."
      AliasTarget:
        DNSName:                        !GetAtt LoadBalancer.DNSName
        EvaluateTargetHealth:           false
        HostedZoneId:                   !GetAtt LoadBalancer.CanonicalHostedZoneID


  LoadBalancer:
    Type:                               "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Name:                             !Sub "${AWS::StackName}"
      Type:                             "application"
      Scheme:                           "internet-facing"
      IpAddressType:                    "ipv4"
      Subnets:                          !Ref PublicSubnets
      SecurityGroups:                   [ !Ref LoadBalancerSecurityGroup ]


  APITargetGroup:
    Type:                               "AWS::ElasticLoadBalancingV2::TargetGroup"
    DependsOn:                          [ LoadBalancer, InvocationPermission ]
    Properties:
      Name:                             !Sub "${AWS::StackName}"
      TargetType:                       "lambda"
      Targets:
        - Id:                           !GetAtt LambdaFunction.Arn
      TargetGroupAttributes:
        - Key:                          lambda.multi_value_headers.enabled
          Value:                        false


  LoadBalancerHTTPListener:
    Type:                               "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      LoadBalancerArn:                  !Ref LoadBalancer
      Protocol:                         "HTTP"
      Port:                             80
      DefaultActions:
        - Type:                         "redirect"
          RedirectConfig:
            Protocol:                   "HTTPS"
            Port:                       443
            StatusCode:                 "HTTP_301"


  LoadBalancerHTTPSListener:
    Type:                               "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      LoadBalancerArn:                  !Ref LoadBalancer
      Protocol:                         "HTTPS"
      Port:                             443
      Certificates:
        - CertificateArn:               !Ref ACMCertificateArn
      DefaultActions:
        - Type:                         "fixed-response"
          FixedResponseConfig:
            StatusCode:                 404


  LoadBalancerHomeDispatchRule:
    Type:                               "AWS::ElasticLoadBalancingV2::ListenerRule"
    Properties: 
      ListenerArn:                      !Ref LoadBalancerHTTPSListener
      Priority:                         10
      Conditions: 
        - Field:                        "path-pattern"
          Values:
            -                           "/"
      Actions: 
        - Type:                         "authenticate-cognito"
          Order:                        100
          AuthenticateCognitoConfig:
            UserPoolArn:                !GetAtt CognitoUserPool.Arn
            UserPoolClientId:           !Ref CognitoUserPoolClient
            UserPoolDomain:             !Ref CognitoUserPoolDomain
            OnUnauthenticatedRequest:   "authenticate"
        - Type:                         "fixed-response"
          Order:                        200
          FixedResponseConfig:
            StatusCode:                 200
            ContentType:                "text/html"
            MessageBody:                |
                                        <!doctype html>
                                        <html lang="en">
                                        <head>
                                            <title>Simple Page</title>
                                        </head>
                                        <body>
                                            <button onclick="pressMe()">Press Me</button>
                                        </body>
                                        <script type="text/javascript">
                                            const rootUrl = window.location.href.replace(/[^/]*$/, "");
                                            const queryUrl = rootUrl + "api/example";

                                            async function pressMe() {
                                                console.log("making remote call");
                                                const request = {
                                                    method: 'POST',
                                                    cache: 'no-cache',
                                                    headers: {
                                                        'Content-Type': 'application/json'
                                                    },
                                                    body: "test data"
                                                };
                                                const response = await fetch(queryUrl, request);
                                                if (response.ok) {
                                                    content = await response.text();
                                                    console.log("response: " + content)
                                                }
                                                else {
                                                    console.log("failed to execute remote call: " + response.status)
                                                }
                                            }
                                        </script>
                                        </html>


  LoadBalancerAPIDispatchRule:
    Type:                               "AWS::ElasticLoadBalancingV2::ListenerRule"
    Properties: 
      ListenerArn:                      !Ref LoadBalancerHTTPSListener
      Priority:                         20
      Conditions: 
        - Field:                        "path-pattern"
          Values:
            -                           "/api/*"
      Actions: 
        - Type:                         "authenticate-cognito"
          Order:                        100
          AuthenticateCognitoConfig:
            UserPoolArn:                !GetAtt CognitoUserPool.Arn
            UserPoolClientId:           !Ref CognitoUserPoolClient
            UserPoolDomain:             !Ref CognitoUserPoolDomain
            OnUnauthenticatedRequest:   "deny"
        - Type:                         "forward"
          Order:                        200
          TargetGroupArn:               !Ref APITargetGroup

Outputs:

  ApplicationEndpoint:
    Description:                        "Endpoint where users can access this application"
    Value:                              !Sub "https://${Hostname}.${DNSDomain}/"
