import apigateway = require ( "@aws-cdk/aws-apigateway" ) ; import cdk = require ( "@aws-cdk/core" ) ; import dynamodb = require ( "@aws-cdk/aws-dynamodb" ) ; import lambda = require ( "@aws-cdk/aws-lambda" ) ; import cognito = require ( "@aws-cdk/aws-cognito" ) ; import iam = require ( "@aws-cdk/aws-iam" ) ; import s3 = require ( "@aws-cdk/aws-s3" ) ; import cloudfront = require ( "@aws-cdk/aws-cloudfront" ) ; import { BillingMode , StreamViewType } from "@aws-cdk/aws-dynamodb" ; import "source-map-support/register" ; import { AuthorizationType } from "@aws-cdk/aws-apigateway" ; import { CfnUserPool , CfnUserPoolIdentityProvider , SignInType , UserPool , UserPoolAttribute } from "@aws-cdk/aws-cognito" ; import { Utils } from "./utils" ; import { Runtime } from "@aws-cdk/aws-lambda" ; import { URL } from "url" ; import { Duration } from "@aws-cdk/core" ; import { Bucket } from "@aws-cdk/aws-s3" ; import { CloudFrontWebDistribution } from "@aws-cdk/aws-cloudfront" ; /**
 * Define a CloudFormation stack that creates a serverless application with
 * Amazon Cognito and an external SAML based IdP
 */ export class BackendStack extends cdk . Stack { constructor ( scope , id , props ? ) { super ( scope , id , props ) ; const domain = Utils . getEnv ( "COGNITO_DOMAIN_NAME" ) ; const identityProviderName = Utils . getEnv ( "IDENTITY_PROVIDER_NAME" , "" ) ; const identityProviderMetadataURLOrFile = Utils . getEnv ( "IDENTITY_PROVIDER_METADATA" , "" ) ; const appFrontendDeployMode = Utils . getEnv ( "APP_FRONTEND_DEPLOY_MODE" , "" ) ; const groupsAttributeName = Utils . getEnv ( "GROUPS_ATTRIBUTE_NAME" , "groups" ) ; const adminsGroupName = Utils . getEnv ( "ADMINS_GROUP_NAME" , "pet-app-admins" ) ; const usersGroupName = Utils . getEnv ( "USERS_GROUP_NAME" , "pet-app-users" ) ; const lambdaMemory = parseInt ( Utils . getEnv ( "LAMBDA_MEMORY" , "128" ) ) ; const nodeRuntime = lambda . Runtime . NODEJS_10_X ; const authorizationHeaderName = "Authorization" ; const groupsAttributeClaimName = "custom:" + groupsAttributeName ; const isModeS3 = appFrontendDeployMode === "s3" ; const isModeCloudfront = appFrontendDeployMode === "cloudfront" ; let appUrl = Utils . getEnv ( "APP_URL" , "" ) ; let uiBucketName = undefined ; let corsOrigin = undefined ; if ( isModeS3 || isModeCloudfront ) { const uiBucket = new s3 . Bucket ( this , 'UIBucket' ) ; uiBucketName = uiBucket . bucketName ; if ( isModeS3 ) { appUrl = "https://" + uiBucket . bucketRegionalDomainName + "/index.html" ; corsOrigin = "https://" + uiBucket . bucketRegionalDomainName ; } else { const distribution = this . createCloudFrontDistribution ( uiBucket ) ; if ( ! appUrl ) { appUrl = "https://" + distribution . domainName ; corsOrigin = "https://" + distribution . domainName ; } } } if ( ! appUrl ) { throw new Error ( `APP_URL environment variable must be defined` ) ; } if ( ! corsOrigin ) { corsOrigin = new URL ( appUrl ) . origin ; } const preTokenGeneration = new lambda . Function ( this , "PreTokenGeneration" , { runtime : nodeRuntime , handler : "index.handler" , code : lambda . Code . fromAsset ( "../lambda/pretokengeneration/dist/src" ) , environment : { GROUPS_ATTRIBUTE_CLAIM_NAME : groupsAttributeClaimName , } , } ) ; const userPool = new cognito . UserPool ( this , id + "Pool" , { signInType : SignInType . EMAIL , autoVerifiedAttributes : [ UserPoolAttribute . EMAIL ] , lambdaTriggers : { preTokenGeneration : preTokenGeneration } } ) ; const userPoolCfn = userPool . node . defaultChild as CfnUserPool ; userPoolCfn . userPoolAddOns = { advancedSecurityMode : "ENFORCED" } ; userPoolCfn . schema = [ { name : groupsAttributeName , attributeDataType : "String" , mutable : true , required : false , stringAttributeConstraints : { maxLength : "2000" } } ] ; new cognito . CfnUserPoolGroup ( this , "AdminsGroup" , { groupName : adminsGroupName , userPoolId : userPool . userPoolId , } ) ; new cognito . CfnUserPoolGroup ( this , "UsersGroup" , { groupName : usersGroupName , userPoolId : userPool . userPoolId , } ) ; const itemsTable = new dynamodb . Table ( this , "ItemsTable" , { billingMode : BillingMode . PAY_PER_REQUEST , serverSideEncryption : true , stream : StreamViewType . NEW_AND_OLD_IMAGES , partitionKey : { name : "id" , type : dynamodb . AttributeType . STRING } } ) ; const usersTable = new dynamodb . Table ( this , "UsersTable" , { billingMode : BillingMode . PAY_PER_REQUEST , serverSideEncryption : true , stream : StreamViewType . NEW_AND_OLD_IMAGES , partitionKey : { name : "username" , type : dynamodb . AttributeType . STRING } , timeToLiveAttribute : "ttl" , } ) ; const apiFunction = new lambda . Function ( this , "APIFunction" , { runtime : nodeRuntime , handler : "index.handler" , code : lambda . Code . fromAsset ( "../lambda/api/dist/src" ) , timeout : Duration . seconds ( 30 ) , memorySize : lambdaMemory , environment : { ITEMS_TABLE_NAME : itemsTable . tableName , USERS_TABLE_NAME : usersTable . tableName , ALLOWED_ORIGIN : corsOrigin , ADMINS_GROUP_NAME : adminsGroupName , USERS_GROUP_NAME : usersGroupName , USER_POOL_ID : userPool . userPoolId , AUTHORIZATION_HEADER_NAME : authorizationHeaderName , } , } ) ; itemsTable . grantReadWriteData ( apiFunction . role ! ) ; usersTable . grantReadWriteData ( apiFunction . role ! ) ; apiFunction . addToRolePolicy ( new iam . PolicyStatement ( { resources : [ userPool . userPoolArn ] , actions : [ "cognito-idp:AdminUserGlobalSignOut" , "cognito-idp:AdminGetUser" ] } ) ) ; const api = new apigateway . RestApi ( this , id + "API" ) ; const integration = new apigateway . LambdaIntegration ( apiFunction , { proxy : true } ) ; const cfnAuthorizer = new apigateway . CfnAuthorizer ( this , id , { name : "CognitoAuthorizer" , type : AuthorizationType . COGNITO , identitySource : "method.request.header." + authorizationHeaderName , restApiId : api . restApiId , providerArns : [ userPool . userPoolArn ] } ) ; const rootResource = api . root ; rootResource . addMethod ( "ANY" , integration ) ; const proxyResource = rootResource . addResource ( "{proxy+}" ) ; const method = proxyResource . addMethod ( "ANY" , integration , { authorizer : { authorizerId : cfnAuthorizer . ref } , authorizationType : AuthorizationType . COGNITO , } ) ; Utils . addCorsOptions ( proxyResource , corsOrigin ) ; Utils . addCorsOptions ( rootResource , corsOrigin ) ; const supportedIdentityProviders = [ "COGNITO" ] ; let cognitoIdp = undefined ; if ( identityProviderMetadataURLOrFile && identityProviderName ) { cognitoIdp = new cognito . CfnUserPoolIdentityProvider ( this , "CognitoIdP" , { providerName : identityProviderName , providerDetails : Utils . isURL ( identityProviderMetadataURLOrFile ) ? { MetadataURL : identityProviderMetadataURLOrFile } : { MetadataFile : identityProviderMetadataURLOrFile } , providerType : "SAML" , attributeMapping : { "email" : "email" , "family_name" : "lastName" , "given_name" : "firstName" , "name" : "firstName" , [ groupsAttributeClaimName ] : "groups" } , userPoolId : userPool . userPoolId } ) ; supportedIdentityProviders . push ( identityProviderName ) ; } const cfnUserPoolClient = new cognito . CfnUserPoolClient ( this , "CognitoAppClient" , { supportedIdentityProviders : supportedIdentityProviders , clientName : "Web" , allowedOAuthFlowsUserPoolClient : true , allowedOAuthFlows : [ "code" ] , allowedOAuthScopes : [ "phone" , "email" , "openid" , "profile" ] , explicitAuthFlows : [ "ALLOW_REFRESH_TOKEN_AUTH" ] , preventUserExistenceErrors : "ENABLED" , generateSecret : false , refreshTokenValidity : 1 , callbackUrLs : [ appUrl ] , logoutUrLs : [ appUrl ] , userPoolId : userPool . userPoolId } ) ; if ( cognitoIdp ) { cfnUserPoolClient . node . addDependency ( cognitoIdp ) ; } const cfnUserPoolDomain = new cognito . CfnUserPoolDomain ( this , "CognitoDomain" , { domain : domain , userPoolId : userPool . userPoolId } ) ; new cdk . CfnOutput ( this , "APIUrlOutput" , { description : "API URL" , value : api . url } ) ; new cdk . CfnOutput ( this , "UserPoolIdOutput" , { description : "UserPool ID" , value : userPool . userPoolId } ) ; new cdk . CfnOutput ( this , "AppClientIdOutput" , { description : "App Client ID" , value : cfnUserPoolClient . ref } ) ; new cdk . CfnOutput ( this , "RegionOutput" , { description : "Region" , value : this . region } ) ; new cdk . CfnOutput ( this , "CognitoDomainOutput" , { description : "Cognito Domain" , value : cfnUserPoolDomain . domain } ) ; new cdk . CfnOutput ( this , "LambdaFunctionName" , { description : "Lambda Function Name" , value : apiFunction . functionName } ) ; new cdk . CfnOutput ( this , "AppUrl" , { description : "The frontend app's URL" , value : appUrl } ) ; if ( uiBucketName ) { new cdk . CfnOutput ( this , "UIBucketName" , { description : "The frontend app's bucket name" , value : uiBucketName } ) ; } } private createCloudFrontDistribution ( uiBucket ) { const cloudFrontOia = new cloudfront . OriginAccessIdentity ( this , 'OIA' , { comment : `OIA for ${ uiBucket . bucketName }` } ) ; const distribution = new cloudfront . CloudFrontWebDistribution ( this , 'UIDistribution' , { originConfigs : [ { s3OriginSource : { s3BucketSource : uiBucket , originAccessIdentity : cloudFrontOia } , behaviors : [ { isDefaultBehavior : true } ] } ] } ) ; return distribution ; } } const app = new cdk . App ( ) ; const stackName = Utils . getEnv ( "STACK_NAME" ) ; const stackAccount = Utils . getEnv ( "STACK_ACCOUNT" ) ; const stackRegion = Utils . getEnv ( "STACK_REGION" ) ; const stackProps = { env : { region : stackRegion , account : stackAccount } } ; const backendStack = new BackendStack ( app , stackName , stackProps ) ; backendStack . templateOptions . transforms = [ "AWS::Serverless-2016-10-31" ] ;