Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cognito): implement user pool and user pool client constructs #1615

Merged
merged 8 commits into from
Feb 5, 2019
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
// AWS::Cognito CloudFormation Resources:
export * from './cognito.generated';

export * from './user-pool';
export * from './user-pool-client';
53 changes: 53 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import cdk = require('@aws-cdk/cdk');
import { CfnUserPoolClient } from './cognito.generated';
import { UserPool } from './user-pool';

export enum AuthFlow {
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
ADMIN_NO_SRP_AUTH = 'ADMIN_NO_SRP_AUTH',
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
CUSTOM_AUTH_FLOW_ONLY = 'CUSTOM_AUTH_FLOW_ONLY',
USER_PASSWORD_AUTH = 'USER_PASSWORD_AUTH'
}

export interface UserPoolClientProps {
/**
* Name of the application client
* @default unique ID
*/
clientName?: string;

/**
* The UserPool resource this client will have access to
*/
userPool: UserPool;
dotxlem marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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, 'UserPoolClient', {
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
clientName: props.clientName || this.node.uniqueId,
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
generateSecret: props.generateSecret || false,
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
userPoolId: props.userPool.poolId,
explicitAuthFlows: props.enabledAuthFlows
});
this.clientId = userPoolClient.userPoolClientId;
}
}
184 changes: 184 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import lambda = require('@aws-cdk/aws-lambda');
import cdk = require('@aws-cdk/cdk');
import { CfnUserPool } from './cognito.generated';

/**
* Standard attributes
*/
export enum Attribute {
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
ADDRESS = 'address',
BIRTHDATE = 'birthdate',
EMAIL = 'email',
FAMILY_NAME = 'family_name',
GENDER = 'gender',
GIVEN_NAME = 'given_name',
LOCALE = 'locale',
MIDDLE_NAME = 'middle_name',
NAME = 'name',
NICKNAME = 'nickname',
PHONE_NUMBER = 'phone_number',
PICTURE = 'picture',
PREFERRED_USERNAME = 'preferred_username',
PROFILE = 'profile',
TIMEZONE = 'timezone',
UPDATED_AT = 'updated_at',
WEBSITE = 'website'
}

export enum SignInType {
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
USERNAME,
EMAIL,
PHONE,
EMAIL_OR_PHONE
}

export interface UserPoolTriggers {
/**
* Creates an authentication challenge.
*/
createAuthChallenge?: lambda.IFunction;
dotxlem marked this conversation as resolved.
Show resolved Hide resolved

/**
* A custom Message AWS Lambda trigger.
*/
customMessage?: lambda.IFunction;

/**
* Defines the authentication challenge.
*/
defineAuthChallenge?: lambda.IFunction;

/**
* A post-authentication AWS Lambda trigger.
*/
postAuthentication?: lambda.IFunction;

/**
* A post-confirmation AWS Lambda trigger.
*/
postConfirmation?: lambda.IFunction;

/**
* A pre-authentication AWS Lambda trigger.
*/
preAuthentication?: lambda.IFunction;

/**
* A pre-registration AWS Lambda trigger.
*/
preSignUp?: lambda.IFunction;

/**
* Verifies the authentication challenge response.
*/
verifyAuthChallengeResponse?: lambda.IFunction;
}

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?: Attribute[];

/**
* Attributes which Cognito will automatically send a verification message to.
* Must be either EMAIL, PHONE, or both.
* @default no auto verification
*/
autoVerifiedAttributes?: Attribute[];

/**
* Lambda functions to use for supported Cognito triggers.
*/
lambdaTriggers?: UserPoolTriggers;
}

/**
* Define a Cognito User Pool
*/
export class UserPool extends cdk.Construct {
/**
* The physical ID of the created user pool resource
*/
public readonly poolId: string;
dotxlem marked this conversation as resolved.
Show resolved Hide resolved

constructor(scope: cdk.Construct, id: string, props: UserPoolProps) {
super(scope, id);

const triggers = props.lambdaTriggers || { };

let aliasAttributes: Attribute[] | undefined;
let usernameAttributes: Attribute[] | 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 === Attribute.EMAIL || a === Attribute.PHONE_NUMBER || a === Attribute.PREFERRED_USERNAME;
})) {
throw new Error(`'usernameAliasAttributes' can only include EMAIL, PHONE_NUMBER, or PREFERRED_USERNAME`);
}

if (props.autoVerifiedAttributes
&& !props.autoVerifiedAttributes.every(a => a === Attribute.EMAIL || a === Attribute.PHONE_NUMBER)) {
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 = [Attribute.EMAIL];
break;

case SignInType.PHONE:
usernameAttributes = [Attribute.PHONE_NUMBER];
break;

case SignInType.EMAIL_OR_PHONE:
usernameAttributes = [Attribute.EMAIL, Attribute.PHONE_NUMBER];
break;

default:
aliasAttributes = props.usernameAliasAttributes;
break;
}

const userPool = new CfnUserPool(this, 'UserPool', {
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
userPoolName: props.poolName || this.node.uniqueId,
usernameAttributes,
aliasAttributes,
autoVerifiedAttributes: props.autoVerifiedAttributes,
lambdaConfig: {
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
createAuthChallenge: triggers.createAuthChallenge && triggers.createAuthChallenge.functionArn,
customMessage: triggers.customMessage && triggers.customMessage.functionArn,
defineAuthChallenge: triggers.defineAuthChallenge && triggers.defineAuthChallenge.functionArn,
postAuthentication: triggers.postAuthentication && triggers.postAuthentication.functionArn,
postConfirmation: triggers.postConfirmation && triggers.postConfirmation.functionArn,
preAuthentication: triggers.preAuthentication && triggers.preAuthentication.functionArn,
preSignUp: triggers.preSignUp && triggers.preSignUp.functionArn,
verifyAuthChallengeResponse: triggers.verifyAuthChallengeResponse && triggers.verifyAuthChallengeResponse.functionArn
}
});
this.poolId = userPool.userPoolId;
}
}
6 changes: 4 additions & 2 deletions packages/@aws-cdk/aws-cognito/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@
"pkglint": "^0.22.0"
},
"dependencies": {
"@aws-cdk/cdk": "^0.22.0"
"@aws-cdk/cdk": "^0.22.0",
"@aws-cdk/aws-lambda": "^0.22.0"
},
"homepage": "https://github.com/awslabs/aws-cdk",
"peerDependencies": {
"@aws-cdk/cdk": "^0.22.0"
"@aws-cdk/cdk": "^0.22.0",
"@aws-cdk/aws-lambda": "^0.22.0"
},
"engines": {
"node": ">= 8.10.0"
Expand Down
8 changes: 0 additions & 8 deletions packages/@aws-cdk/aws-cognito/test/test.cognito.ts

This file was deleted.

24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-cognito/test/test.user-pool-client.ts
Original file line number Diff line number Diff line change
@@ -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) {
dotxlem marked this conversation as resolved.
Show resolved Hide resolved
// 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.poolId)
}));

test.done();
}
};
74 changes: 74 additions & 0 deletions packages/@aws-cdk/aws-cognito/test/test.user-pool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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();

// WHEN
new cognito.UserPool(stack, 'Pool', {
poolName: 'myPool'
});

// THEN
expect(stack).to(haveResourceLike('AWS::Cognito::UserPool', {
UserPoolName: 'myPool'
}));

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.Attribute.PREFERRED_USERNAME]
});
};

// 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.Attribute.GIVEN_NAME]
});
};

// 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.Attribute.EMAIL, cognito.Attribute.GENDER]
});
};

// THEN
test.throws(() => toThrow(), /'autoVerifiedAttributes' can only include EMAIL or PHONE_NUMBER/);
test.done();
}
};