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());
}