diff --git a/packages/@aws-cdk/aws-cognito/lib/index.ts b/packages/@aws-cdk/aws-cognito/lib/index.ts index 1e585f2ae1c7e..11171b83add28 100644 --- a/packages/@aws-cdk/aws-cognito/lib/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/index.ts @@ -1,2 +1,5 @@ // AWS::Cognito CloudFormation Resources: export * from './cognito.generated'; + +export * from './user-pool'; +export * from './user-pool-client'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts new file mode 100644 index 0000000000000..b01375ef17405 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -0,0 +1,67 @@ +import cdk = require('@aws-cdk/cdk'); +import { CfnUserPoolClient } from './cognito.generated'; +import { IUserPool } from './user-pool'; + +/** + * Types of authentication flow + */ +export enum AuthFlow { + /** + * Enable flow for server-side or admin authentication (no client app) + */ + AdminNoSrp = 'ADMIN_NO_SRP_AUTH', + + /** + * Enable custom authentication flow + */ + CustomFlowOnly = 'CUSTOM_AUTH_FLOW_ONLY', + + /** + * Enable auth using username & password + */ + UserPassword = 'USER_PASSWORD_AUTH' +} + +export interface UserPoolClientProps { + /** + * Name of the application client + * @default cloudformation generated name + */ + clientName?: string; + + /** + * The UserPool resource this client will have access to + */ + userPool: IUserPool; + + /** + * Whether to generate a client secret + * @default false + */ + generateSecret?: boolean; + + /** + * List of enabled authentication flows + * @default no enabled flows + */ + enabledAuthFlows?: AuthFlow[] +} + +/** + * Define a UserPool App Client + */ +export class UserPoolClient extends cdk.Construct { + public readonly clientId: string; + + constructor(scope: cdk.Construct, id: string, props: UserPoolClientProps) { + super(scope, id); + + const userPoolClient = new CfnUserPoolClient(this, 'Resource', { + clientName: props.clientName, + generateSecret: props.generateSecret, + userPoolId: props.userPool.userPoolId, + explicitAuthFlows: props.enabledAuthFlows + }); + this.clientId = userPoolClient.userPoolClientId; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts new file mode 100644 index 0000000000000..4612a218267d9 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -0,0 +1,532 @@ +import iam = require('@aws-cdk/aws-iam'); +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { CfnUserPool } from './cognito.generated'; + +/** + * Standard attributes + * Specified following the OpenID Connect spec + * @see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + */ +export enum UserPoolAttribute { + /** + * End-User's preferred postal address. + */ + Address = 'address', + + /** + * End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD format. + * The year MAY be 0000, indicating that it is omitted. + * To represent only the year, YYYY format is allowed. + */ + Birthdate = 'birthdate', + + /** + * End-User's preferred e-mail address. + * Its value MUST conform to the RFC 5322 [RFC5322] addr-spec syntax. + */ + Email = 'email', + + /** + * Surname(s) or last name(s) of the End-User. + * Note that in some cultures, people can have multiple family names or no family name; + * all can be present, with the names being separated by space characters. + */ + FamilyName = 'family_name', + + /** + * End-User's gender. + */ + Gender = 'gender', + + /** + * Given name(s) or first name(s) of the End-User. + * Note that in some cultures, people can have multiple given names; + * all can be present, with the names being separated by space characters. + */ + GivenName = 'given_name', + + /** + * End-User's locale, represented as a BCP47 [RFC5646] language tag. + * This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase + * and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. + * For example, en-US or fr-CA. + */ + Locale = 'locale', + + /** + * Middle name(s) of the End-User. + * Note that in some cultures, people can have multiple middle names; + * all can be present, with the names being separated by space characters. + * Also note that in some cultures, middle names are not used. + */ + MiddleName = 'middle_name', + + /** + * End-User's full name in displayable form including all name parts, + * possibly including titles and suffixes, ordered according to the End-User's locale and preferences. + */ + Name = 'name', + + /** + * Casual name of the End-User that may or may not be the same as the given_name. + * For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. + */ + Nickname = 'nickname', + + /** + * End-User's preferred telephone number. + * E.164 [E.164] is RECOMMENDED as the format of this Claim, for example, +1 (425) 555-1212 or +56 (2) 687 2400. + * If the phone number contains an extension, it is RECOMMENDED that the extension be represented using the + * RFC 3966 [RFC3966] extension syntax, for example, +1 (604) 555-1234;ext=5678. + */ + PhoneNumber = 'phone_number', + + /** + * URL of the End-User's profile picture. + * This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), + * rather than to a Web page containing an image. + * Note that this URL SHOULD specifically reference a profile photo of the End-User + * suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User + */ + Picture = 'picture', + + /** + * Shorthand name by which the End-User wishes to be referred to. + */ + PreferredUsername = 'preferred_username', + + /** + * URL of the End-User's profile page. The contents of this Web page SHOULD be about the End-User. + */ + Profile = 'profile', + + /** + * The End-User's time zone + */ + Timezone = 'timezone', + + /** + * Time the End-User's information was last updated. + * Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z + * as measured in UTC until the date/time. + */ + UpdatedAt = 'updated_at', + + /** + * URL of the End-User's Web page or blog. + * This Web page SHOULD contain information published by the End-User or an organization that the End-User is affiliated with. + */ + Website = 'website' +} + +/** + * Methods of user sign-in + */ +export enum SignInType { + /** + * End-user will sign in with a username, with optional aliases + */ + Username, + + /** + * End-user will sign in using an email address + */ + Email, + + /** + * End-user will sign in using a phone number + */ + Phone, + + /** + * End-user will sign in using either an email address or phone number + */ + EmailOrPhone +} + +export interface UserPoolTriggers { + /** + * Creates an authentication challenge. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-create-auth-challenge.html + */ + createAuthChallenge?: lambda.IFunction; + + /** + * A custom Message AWS Lambda trigger. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-message.html + */ + customMessage?: lambda.IFunction; + + /** + * Defines the authentication challenge. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-define-auth-challenge.html + */ + defineAuthChallenge?: lambda.IFunction; + + /** + * A post-authentication AWS Lambda trigger. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-authentication.html + */ + postAuthentication?: lambda.IFunction; + + /** + * A post-confirmation AWS Lambda trigger. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-confirmation.html + */ + postConfirmation?: lambda.IFunction; + + /** + * A pre-authentication AWS Lambda trigger. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-authentication.html + */ + preAuthentication?: lambda.IFunction; + + /** + * A pre-registration AWS Lambda trigger. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html + */ + preSignUp?: lambda.IFunction; + + /** + * Verifies the authentication challenge response. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-verify-auth-challenge-response.html + */ + verifyAuthChallengeResponse?: lambda.IFunction; + + /** + * Index signature + */ + [trigger: string]: lambda.IFunction | undefined; +} + +export interface UserPoolProps { + /** + * Name of the user pool + * @default unique ID + */ + poolName?: string; + + /** + * Method used for user registration & sign in. + * Allows either username with aliases OR sign in with email, phone, or both. + * @default SignInType.USERNAME + */ + signInType?: SignInType; + + /** + * Attributes to allow as username alias. + * Only valid if signInType is USERNAME + * @default no alias + */ + usernameAliasAttributes?: UserPoolAttribute[]; + + /** + * Attributes which Cognito will automatically send a verification message to. + * Must be either EMAIL, PHONE, or both. + * @default no auto verification + */ + autoVerifiedAttributes?: UserPoolAttribute[]; + + /** + * Lambda functions to use for supported Cognito triggers. + */ + lambdaTriggers?: UserPoolTriggers; +} + +export interface UserPoolImportProps { + /** + * The ID of an existing user pool + */ + userPoolId: string; + + /** + * The ARN of the imported user pool + */ + userPoolArn: string; + + /** + * The provider name of the imported user pool + */ + userPoolProviderName: string; + + /** + * The URL of the imported user pool + */ + userPoolProviderUrl: string; +} + +export interface IUserPool extends cdk.IConstruct { + /** + * The physical ID of this user pool resource + */ + readonly userPoolId: string; + + /** + * The ARN of this user pool resource + */ + readonly userPoolArn: string; + + /** + * The provider name of this user pool resource + */ + readonly userPoolProviderName: string; + + /** + * The provider URL of this user pool resource + */ + readonly userPoolProviderUrl: string; + + /** + * Exports a User Pool from this stack + * @returns user pool props that can be imported into another stack + */ + export(): UserPoolImportProps; +} + +/** + * Define a Cognito User Pool + */ +export class UserPool extends cdk.Construct implements IUserPool { + /** + * Import an existing user pool resource + * @param scope Parent construct + * @param id Construct ID + * @param props Imported user pool properties + */ + public static import(scope: cdk.Construct, id: string, props: UserPoolImportProps): IUserPool { + return new ImportedUserPool(scope, id, props); + } + + /** + * The physical ID of this user pool resource + */ + public readonly userPoolId: string; + + /** + * The ARN of the user pool + */ + public readonly userPoolArn: string; + + /** + * User pool provider name + */ + public readonly userPoolProviderName: string; + + /** + * User pool provider URL + */ + public readonly userPoolProviderUrl: string; + + private triggers: CfnUserPool.LambdaConfigProperty = { }; + + constructor(scope: cdk.Construct, id: string, props: UserPoolProps) { + super(scope, id); + + let aliasAttributes: UserPoolAttribute[] | undefined; + let usernameAttributes: UserPoolAttribute[] | undefined; + + if (props.usernameAliasAttributes != null && props.signInType !== SignInType.Username) { + throw new Error(`'usernameAliasAttributes' can only be set with a signInType of 'USERNAME'`); + } + + if (props.usernameAliasAttributes + && !props.usernameAliasAttributes.every(a => { + return a === UserPoolAttribute.Email || a === UserPoolAttribute.PhoneNumber || a === UserPoolAttribute.PreferredUsername; + })) { + throw new Error(`'usernameAliasAttributes' can only include EMAIL, PHONE_NUMBER, or PREFERRED_USERNAME`); + } + + if (props.autoVerifiedAttributes + && !props.autoVerifiedAttributes.every(a => a === UserPoolAttribute.Email || a === UserPoolAttribute.PhoneNumber)) { + throw new Error(`'autoVerifiedAttributes' can only include EMAIL or PHONE_NUMBER`); + } + + switch (props.signInType) { + case SignInType.Username: + aliasAttributes = props.usernameAliasAttributes; + break; + + case SignInType.Email: + usernameAttributes = [UserPoolAttribute.Email]; + break; + + case SignInType.Phone: + usernameAttributes = [UserPoolAttribute.PhoneNumber]; + break; + + case SignInType.EmailOrPhone: + usernameAttributes = [UserPoolAttribute.Email, UserPoolAttribute.PhoneNumber]; + break; + + default: + aliasAttributes = props.usernameAliasAttributes; + break; + } + + if (props.lambdaTriggers) { + for (const t of Object.keys(props.lambdaTriggers)) { + const trigger = props.lambdaTriggers[t]; + if (trigger !== undefined) { + this.addLambdaPermission(trigger as lambda.IFunction, t); + (this.triggers as any)[t] = (trigger as lambda.IFunction).functionArn; + } + } + } + + const userPool = new CfnUserPool(this, 'Resource', { + userPoolName: props.poolName || this.node.uniqueId, + usernameAttributes, + aliasAttributes, + autoVerifiedAttributes: props.autoVerifiedAttributes, + lambdaConfig: new cdk.Token(() => this.triggers) + }); + this.userPoolId = userPool.userPoolId; + this.userPoolArn = userPool.userPoolArn; + this.userPoolProviderName = userPool.userPoolProviderName; + this.userPoolProviderUrl = userPool.userPoolProviderUrl; + } + + /** + * Attach 'Create Auth Challenge' trigger + * Grants access from cognito-idp.amazonaws.com to the lambda + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-create-auth-challenge.html + * @param fn the lambda function to attach + */ + public onCreateAuthChallenge(fn: lambda.IFunction): void { + this.addLambdaPermission(fn, 'CreateAuthChallenge'); + this.triggers.createAuthChallenge = fn.functionArn; + } + + /** + * Attach 'Custom Message' trigger + * Grants access from cognito-idp.amazonaws.com to the lambda + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-message.html + * @param fn the lambda function to attach + */ + public onCustomMessage(fn: lambda.IFunction): void { + this.addLambdaPermission(fn, 'CustomMessage'); + this.triggers.customMessage = fn.functionArn; + } + + /** + * Attach 'Define Auth Challenge' trigger + * Grants access from cognito-idp.amazonaws.com to the lambda + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-define-auth-challenge.html + * @param fn the lambda function to attach + */ + public onDefineAuthChallenge(fn: lambda.IFunction): void { + this.addLambdaPermission(fn, 'DefineAuthChallenge'); + this.triggers.defineAuthChallenge = fn.functionArn; + } + + /** + * Attach 'Post Authentication' trigger + * Grants access from cognito-idp.amazonaws.com to the lambda + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-authentication.html + * @param fn the lambda function to attach + */ + public onPostAuthentication(fn: lambda.IFunction): void { + this.addLambdaPermission(fn, 'PostAuthentication'); + this.triggers.postAuthentication = fn.functionArn; + } + + /** + * Attach 'Post Confirmation' trigger + * Grants access from cognito-idp.amazonaws.com to the lambda + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-confirmation.html + * @param fn the lambda function to attach + */ + public onPostConfirmation(fn: lambda.IFunction): void { + this.addLambdaPermission(fn, 'PostConfirmation'); + this.triggers.postConfirmation = fn.functionArn; + } + + /** + * Attach 'Pre Authentication' trigger + * Grants access from cognito-idp.amazonaws.com to the lambda + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-authentication.html + * @param fn the lambda function to attach + */ + public onPreAuthentication(fn: lambda.IFunction): void { + this.addLambdaPermission(fn, 'PreAuthentication'); + this.triggers.preAuthentication = fn.functionArn; + } + + /** + * Attach 'Pre Sign Up' trigger + * Grants access from cognito-idp.amazonaws.com to the lambda + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html + * @param fn the lambda function to attach + */ + public onPreSignUp(fn: lambda.IFunction): void { + this.addLambdaPermission(fn, 'PreSignUp'); + this.triggers.preSignUp = fn.functionArn; + } + + /** + * Attach 'Verify Auth Challenge Response' trigger + * Grants access from cognito-idp.amazonaws.com to the lambda + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-verify-auth-challenge-response.html + * @param fn the lambda function to attach + */ + public onVerifyAuthChallengeResponse(fn: lambda.IFunction): void { + this.addLambdaPermission(fn, 'VerifyAuthChallengeResponse'); + this.triggers.verifyAuthChallengeResponse = fn.functionArn; + } + + public export(): UserPoolImportProps { + return { + userPoolId: new cdk.Output(this, 'UserPoolId', { value: this.userPoolId }).makeImportValue().toString(), + userPoolArn: new cdk.Output(this, 'UserPoolArn', { value: this.userPoolArn }).makeImportValue().toString(), + userPoolProviderName: new cdk.Output(this, 'UserPoolProviderName', { value: this.userPoolProviderName }).makeImportValue().toString(), + userPoolProviderUrl: new cdk.Output(this, 'UserPoolProviderUrl', { value: this.userPoolProviderUrl }).makeImportValue().toString() + }; + } + + private addLambdaPermission(fn: lambda.IFunction, name: string): void { + const normalize = name.charAt(0).toUpperCase() + name.slice(1); + fn.addPermission(`${normalize}Cognito`, { + principal: new iam.ServicePrincipal('cognito-idp.amazonaws.com'), + sourceArn: this.userPoolArn + }); + } +} + +/** + * Define a user pool which has been declared in another stack + */ +class ImportedUserPool extends cdk.Construct implements IUserPool { + /** + * The ID of an existing user pool + */ + public readonly userPoolId: string; + + /** + * The ARN of the imported user pool + */ + public readonly userPoolArn: string; + + /** + * The provider name of the imported user pool + */ + public readonly userPoolProviderName: string; + + /** + * The URL of the imported user pool + */ + public readonly userPoolProviderUrl: string; + + constructor(scope: cdk.Construct, id: string, private readonly props: UserPoolImportProps) { + super(scope, id); + + this.userPoolId = props.userPoolId; + this.userPoolArn = props.userPoolArn; + this.userPoolProviderName = props.userPoolProviderName; + this.userPoolProviderUrl = props.userPoolProviderUrl; + } + + public export(): UserPoolImportProps { + return this.props; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index dca22bc36e826..287a3b705cbc3 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -60,11 +60,14 @@ "pkglint": "^0.23.0" }, "dependencies": { - "@aws-cdk/cdk": "^0.23.0" + "@aws-cdk/cdk": "^0.23.0", + "@aws-cdk/aws-lambda": "^0.23.0", + "@aws-cdk/aws-iam": "^0.23.0" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { - "@aws-cdk/cdk": "^0.23.0" + "@aws-cdk/cdk": "^0.23.0", + "@aws-cdk/aws-lambda": "^0.23.0" }, "engines": { "node": ">= 8.10.0" diff --git a/packages/@aws-cdk/aws-cognito/test/test.cognito.ts b/packages/@aws-cdk/aws-cognito/test/test.cognito.ts deleted file mode 100644 index 820f6b467f38f..0000000000000 --- a/packages/@aws-cdk/aws-cognito/test/test.cognito.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); diff --git a/packages/@aws-cdk/aws-cognito/test/test.user-pool-client.ts b/packages/@aws-cdk/aws-cognito/test/test.user-pool-client.ts new file mode 100644 index 0000000000000..fe7c6e8008aa3 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/test.user-pool-client.ts @@ -0,0 +1,24 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import cognito = require('../lib'); + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const pool = new cognito.UserPool(stack, 'Pool', { }); + + // WHEN + new cognito.UserPoolClient(stack, 'Client', { + userPool: pool + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Cognito::UserPoolClient', { + UserPoolId: pool.node.resolve(pool.userPoolId) + })); + + test.done(); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/test.user-pool.ts b/packages/@aws-cdk/aws-cognito/test/test.user-pool.ts new file mode 100644 index 0000000000000..bfae0eb1513e1 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/test.user-pool.ts @@ -0,0 +1,184 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import cognito = require('../lib'); + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new cognito.UserPool(stack, 'Pool', { + poolName: 'myPool' + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'myPool' + })); + + test.done(); + }, + + 'lambda triggers are defined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + }); + + // WHEN + const pool = new cognito.UserPool(stack, 'Pool', { + lambdaTriggers: { + preSignUp: fn + } + }); + pool.onCustomMessage(fn); + + // THEN + expect(stack).to(haveResourceLike('AWS::Cognito::UserPool', { + LambdaConfig: { + PreSignUp: fn.node.resolve(fn.functionArn), + CustomMessage: fn.node.resolve(fn.functionArn) + } + })); + + test.done(); + }, + + 'on* API correctly appends triggers'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + }); + + // WHEN + const pool = new cognito.UserPool(stack, 'Pool', { }); + pool.onCreateAuthChallenge(fn); + pool.onCustomMessage(fn); + pool.onDefineAuthChallenge(fn); + pool.onPostAuthentication(fn); + pool.onPostConfirmation(fn); + pool.onPreAuthentication(fn); + pool.onPreSignUp(fn); + pool.onVerifyAuthChallengeResponse(fn); + + // THEN + expect(stack).to(haveResourceLike('AWS::Cognito::UserPool', { + LambdaConfig: { + CreateAuthChallenge: fn.node.resolve(fn.functionArn), + CustomMessage: fn.node.resolve(fn.functionArn), + DefineAuthChallenge: fn.node.resolve(fn.functionArn), + PostAuthentication: fn.node.resolve(fn.functionArn), + PostConfirmation: fn.node.resolve(fn.functionArn), + PreAuthentication: fn.node.resolve(fn.functionArn), + PreSignUp: fn.node.resolve(fn.functionArn), + VerifyAuthChallengeResponse: fn.node.resolve(fn.functionArn) + } + })); + + test.done(); + }, + + 'lambdas are given cognito service grant'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + }); + + // WHEN + new cognito.UserPool(stack, 'Pool', { + lambdaTriggers: { + preSignUp: fn + } + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Lambda::Permission', { + FunctionName: fn.node.resolve(fn.functionName), + Principal: 'cognito-idp.amazonaws.com' + })); + + test.done(); + }, + + 'set sign in type'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new cognito.UserPool(stack, 'Pool', { + signInType: cognito.SignInType.Email, + autoVerifiedAttributes: [cognito.UserPoolAttribute.Email] + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Cognito::UserPool', { + UsernameAttributes: ['email'], + AutoVerifiedAttributes: ['email'] + })); + + test.done(); + }, + + 'usernameAliasAttributes require signInType of USERNAME'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const toThrow = () => { + new cognito.UserPool(stack, 'Pool', { + signInType: cognito.SignInType.Email, + usernameAliasAttributes: [cognito.UserPoolAttribute.PreferredUsername] + }); + }; + + // THEN + test.throws(() => toThrow(), /'usernameAliasAttributes' can only be set with a signInType of 'USERNAME'/); + test.done(); + }, + + 'usernameAliasAttributes must be one or more of EMAIL, PHONE_NUMBER, or PREFERRED_USERNAME'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const toThrow = () => { + new cognito.UserPool(stack, 'Pool', { + signInType: cognito.SignInType.Username, + usernameAliasAttributes: [cognito.UserPoolAttribute.GivenName] + }); + }; + + // THEN + test.throws(() => toThrow(), /'usernameAliasAttributes' can only include EMAIL, PHONE_NUMBER, or PREFERRED_USERNAME/); + test.done(); + }, + + 'autoVerifiedAttributes must be one or more of EMAIL or PHONE_NUMBER'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const toThrow = () => { + new cognito.UserPool(stack, 'Pool', { + signInType: cognito.SignInType.Email, + autoVerifiedAttributes: [cognito.UserPoolAttribute.Email, cognito.UserPoolAttribute.Gender] + }); + }; + + // THEN + test.throws(() => toThrow(), /'autoVerifiedAttributes' can only include EMAIL or PHONE_NUMBER/); + test.done(); + } +}; \ No newline at end of file