constructor()

in lib/cognito-idp-stack.ts [93:345]


    constructor(scope: Construct, id: string, props: CognitoIdpStackProps) {
        super(scope, id, props);

        if (!props.env) {
            throw Error('props.env is required');
        }

        if (!props.env.region) {
            throw Error('props.env.region is required');
        }

        if (!props.env.account) {
            throw Error('props.env.account is required');
        }

        const region = props.env.region;
        const accountId = props.env.account;

        // Users Table - Store basic user details we get from Cognito
        const userTable = new dynamodb.Table(this, 'UsersTable', {
            partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
            billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
            pointInTimeRecovery: true,
            removalPolicy: cdk.RemovalPolicy.RETAIN
        });

        // Index on username
        userTable.addGlobalSecondaryIndex({
            indexName: 'username-index',
            partitionKey: {
                name: 'username',
                type: dynamodb.AttributeType.STRING
            },
            projectionType: dynamodb.ProjectionType.ALL
        });

        // Output the name of the user table
        const userTableOut = new cdk.CfnOutput(this, 'UserTableName', {
            value: userTable.tableName,
            exportName: 'CognitoIdpUserTableName',
        });

        // Cognito User Pool
        const userPool = new cognito.UserPool(this, 'CognitoIDPUserPool', {
            selfSignUpEnabled: false,
            signInAliases: {
                email: true,
                username: true
            },
            standardAttributes: {
                email: {
                    mutable: true,
                    required: true
                },
                givenName: {
                    mutable: true,
                    required: true
                },
                familyName: {
                    mutable: true,
                    required: true
                }
            }
        });

        // Output the User Pool ID
        const userPoolOut = new cdk.CfnOutput(this, 'CognitoIDPUserPoolOut', {
            value: userPool.userPoolId,
            exportName: 'CognitoIDPUserPoolId'
        });

        // Set up an admin group in the user pool
        const adminsGroup = new cognito.CfnUserPoolGroup(this, "AdminsGroup", {
            userPoolId: userPool.userPoolId
        });

        // We will ask the IDP to redirect back to our domain's index page
        const redirectUri = `https://${props.webDomainName}`;

        // Amazon Federate Client Secret
        const secret = secrets.Secret.fromSecretAttributes(this, 'FederateSecret', {
            secretCompleteArn: props.facebookSecretArn,
        });

        // Facebook IDP
        const idp = new cognito.UserPoolIdentityProviderFacebook(this, 'FacebookIDP', {
            clientId: props.facebookAppId,
            clientSecret: secret.secretValue.toString(),
            scopes: ['email'],
            userPool,
            attributeMapping: {
                email: cognito.ProviderAttribute.FACEBOOK_EMAIL,
                familyName: cognito.ProviderAttribute.FACEBOOK_LAST_NAME,
                givenName: cognito.ProviderAttribute.FACEBOOK_FIRST_NAME
            }
        });

        // Configure the user pool client application 
        const userPoolClient = new cognito.UserPoolClient(this, 'CognitoAppClient', {
            userPool,
            authFlows: {
                userPassword: true
            },
            oAuth: {
                flows: {
                    authorizationCodeGrant: true
                },
                scopes: [
                    cognito.OAuthScope.PHONE,
                    cognito.OAuthScope.EMAIL,
                    cognito.OAuthScope.PROFILE,
                    cognito.OAuthScope.OPENID
                ],
                callbackUrls: [redirectUri]
                // TODO - What about logoutUrls?
            },
            generateSecret: false,
            userPoolClientName: 'Web',
            supportedIdentityProviders: [cognito.UserPoolClientIdentityProvider.FACEBOOK]
        });

        // Output the User Pool App Client ID
        const userPoolClientOut = new cdk.CfnOutput(this, 'CognitoIDPUserPoolClientOut', {
            value: userPoolClient.userPoolClientId,
            exportName: 'CognitoIDPUserPoolClientId'
        });

        // Make sure the user pool client is created after the IDP
        userPoolClient.node.addDependency(idp);

        // Our cognito domain name
        const cognitoDomainPrefix =
            `${props.webDomainName}`.toLowerCase().replace(/[.]/g, "-");

        // Add the domain to the user pool
        userPool.addDomain('CognitoDomain', {
            cognitoDomain: {
                domainPrefix: cognitoDomainPrefix,
            },
        });

        // Configure the lambda functions and REST API

        /**
         * This function grants access to resources to our lambda functions.
         */
        const g = (f: lambda.Function) => {

            // someBucket.grantReadWrite(f);

            const tables = [userTable];

            for (const table of tables) {
                table.grantReadWriteData(f);

                // Give permissions to indexes manually
                f.role?.addToPrincipalPolicy(new iam.PolicyStatement({
                    actions: ['dynamodb:*'],
                    resources: [`${table.tableArn}/index/*`],
                }));
            }
        };

        // Auth
        const handlers: ResourceHandlerProps[] = [];
        handlers.push(new ResourceHandlerProps('decode-verify-jwt', 'get', false, g));

        // Users
        handlers.push(new ResourceHandlerProps('users', 'get', true, g));
        handlers.push(new ResourceHandlerProps('user/{userId}', 'get', true, g));
        handlers.push(new ResourceHandlerProps('user/{userId}', 'delete', true, g));
        handlers.push(new ResourceHandlerProps('user', 'post', true, g));
        handlers.push(new ResourceHandlerProps('userbyusername/{username}', 'get', true, g));

        // The resource handler can't currently handle something like this:
        //
        // thing/{thingId}/otherThing/{otherId}
        //
        // If you need that, do:
        //
        // thing/{thingId}?otherThing=otherId

        // Create the REST API with an L3 construct included in this example repo.
        // (See cognito-rest-api.ts)
        const api = new CognitoRestApi(this, this.stackName, {
            domainName: props.apiDomainName,
            certificateArn: props.apiCertificateArn,
            lambdaFunctionDirectory: './dist/lambda',
            userPool,
            cognitoRedirectUri: `https://${props.webDomainName}`,
            cognitoDomainPrefix,
            cognitoAppClientId: userPoolClient.userPoolClientId,
            cognitoRegion: region,
            additionalEnvVars: {
                "USER_TABLE": userTable.tableName
            },
            resourceHandlers: handlers,
            hostedZoneId: props.apiHostedZoneId
        });

        // Static web site created by an L3 construct included in this example repo
        // (See static-site.ts)
        const site = new StaticSite(this, 'StaticSite', {
            domainName: props.webDomainName,
            certificateArn: props.webCertificateArn,
            contentPath: './dist/web',
            hostedZoneId: props.hostedZoneId
        });

        // Create a custom resource that writes out the config file for the web site.
        // (The web site needs deploy-time values, so this fixes some of the chicken 
        // and egg problems with the .env file)
        const onEvent = new lambda.Function(this, 'CreateConfigHandler', {
            runtime: lambda.Runtime.NODEJS_12_X,
            code: lambda.Code.fromAsset('./dist/lambda'),
            handler: `create-config.handler`,
            memorySize: 1536,
            timeout: cdk.Duration.minutes(5),
            description: `${this.stackName} Static Site Config`,
            environment: {
                'S3_BUCKET_NAME': site.getBucketName(),
                'API_DOMAIN': props.apiDomainName,
                'COGNITO_DOMAIN_PREFIX': cognitoDomainPrefix,
                'COGNITO_REGION': region,
                'COGNITO_APP_CLIENT_ID': userPoolClient.userPoolClientId,
                'COGNITO_REDIRECT_URI': props.cognitoRedirectUri,
                'FACEBOOK_APP_ID': props.facebookAppId,
                'FACEBOOK_VERSION': props.facebookApiVersion
            }
        });

        site.grantAccessTo(onEvent);

        // Create a provider
        const provider = new cr.Provider(this, 'ConfigFileProvider', {
            onEventHandler: onEvent
        });

        // Create the custom resource
        const customResource = new cdk.CustomResource(this, 'ConfigFileResource', {
            serviceToken: provider.serviceToken,
            properties: {
                'FORCE_UPDATE': new Date().toISOString()
            }
        });

        // FORCE_UPDATE forces the custom resource to update the config file on each deploy

        // TODO - Can we set the logical id of the custom resource every time the deployment changes?

        customResource.node.addDependency(site.getDeployment());

    }