Skip to content

Commit

Permalink
feat(cognito): user pool verification and invitation messages
Browse files Browse the repository at this point in the history
  • Loading branch information
Niranjan Jayakar committed Feb 14, 2020
1 parent 20806a8 commit a3d69dc
Show file tree
Hide file tree
Showing 6 changed files with 494 additions and 10 deletions.
93 changes: 93 additions & 0 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,97 @@
---
<!--END STABILITY BANNER-->

[Amazon Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html) provides
authentication, authorization, and user management for your web and mobile apps. Your users can sign in directly with a
user name and password, or through a third party such as Facebook, Amazon, Google or Apple.

The two main components of Amazon Cognito are [user
pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) and [identity
pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html). User pools are user directories
that provide sign-up and sign-in options for your app users. Identity pools enable you to grant your users access to
other AWS services.

This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.

## User Pools

User pools allow creating and managing your own directory of users that can sign up and sign in. They enable easy
integration with social identity providers such as Facebook, Google, Amazon, Microsoft Active Directory, etc. through
SAML.

Using the CDK, a new user pool can be created as part of the stack using the construct's constructor. You may specify
the `userPoolName` to give your own identifier to the user pool. If not, CloudFormation will generate a name.

```ts
new UserPool(this, 'myuserpool', {
userPoolName: 'myawesomeapp-userpool',
});
```

### Sign Up

Users can either be signed up by the app's administrators or can sign themselves up. Once a user has signed up, their
account needs to be confirmed. Cognito provides several ways to sign users up and confirm their accounts. Learn more
about [user sign up here](https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html).

When a user signs up, email and SMS messages are used to verify their account and contact methods. The following code
snippet configures a user pool with properties relevant to these verification messages -

```ts
new UserPool(this, 'myuserpool', {
// ...
selfSignUpEnabled: true,
userVerification: {
emailSubject: 'Verify your email for our awesome app!',
emailBody: 'Hello {username}, Thanks for signing up to our awesome app! Your verification code is {####}',
emailStyle: VerificationEmailStyle.CODE,
smsMessage: 'Hello {username}, Thanks for signing up to our awesome app! Your verification code is {####}',
}
});
```

By default, self sign up is disabled. Learn more about [email and SMS verification messages
here](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-customizations.html).

Besides users signing themselves up, an administrator of any user pool can sign users up. The user then receives an
invitation to join the user pool. The following code snippet configures a user pool with properties relevant to the
invitation messages -

```ts
new UserPool(this, 'myuserpool', {
// ...
userInvitation: {
emailSubject: 'Invite to join our awesome app!',
emailBody: 'Hello {username}, you have been invited to join our awesome app! Your temporary password is {####}',
smsMessage: 'Your temporary password for our awesome app is {####}'
}
});
```

All email subjects, bodies and SMS messages for both invitation and verification support Cognito's message templating.
Learn more about [message templates
here](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-templates.html).

### Security

Cognito sends various messages to its users via SMS, for different actions, ranging from account verification to
marketing. In order to send SMS messages, Cognito needs an IAM role that it can assume, with permissions that allow it
to send SMS messages. By default, CDK will create this IAM role but can also be explicily specified to an existing IAM
role using the `smsRole` property.

```ts
import { Role } from '@aws-cdk/aws-iam';

const poolSmsRole = new Role(this, 'userpoolsmsrole', { /* ... */ });

new UserPool(this, 'myuserpool', {
// ...
smsRole: poolSmsRole,
smsRoleExternalId: 'c87467be-4f34-11ea-b77f-2e728ce88125'
});
```

When the `smsRole` property is specified, the `smsRoleExternalId` may also be specified. The value of
`smsRoleExternalId` will be used as the `sts:ExternalId` when the Cognito service assumes the role. In turn, the role's
assume role policy should be configured to accept this value as the ExternalId. Learn more about [ExternalId
here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html).
184 changes: 181 additions & 3 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as iam from '@aws-cdk/aws-iam';
import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, IResource, Lazy, Resource } from '@aws-cdk/core';
import { CfnUserPool } from './cognito.generated';
Expand Down Expand Up @@ -212,6 +212,78 @@ export interface UserPoolTriggers {
[trigger: string]: lambda.IFunction | undefined;
}

/**
* The email verification style
*/
export enum VerificationEmailStyle {
/** Verify email via code */
CODE = 'CONFIRM_WITH_CODE',
/** Verify email via link */
LINK = 'CONFIRM_WITH_LINK',
}

/**
* User pool configuration for user self sign up.
*/
export interface UserVerificationConfig {
/**
* The email subject template for the verification email sent to the user upon sign up.
* See https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-templates.html to
* learn more about message templates.
* @default 'Verify your new account'
*/
readonly emailSubject?: string;

/**
* The email body template for the verification email sent to the user upon sign up.
* See https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-templates.html to
* learn more about message templates.
* @default 'Hello {username}, Your verification code is {####}'
*/
readonly emailBody?: string;

/**
* Emails can be verified either using a code or a link.
* Learn more at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-email-verification-message-customization.html
* @default VerificationEmailStyle.CODE
*/
readonly emailStyle?: VerificationEmailStyle;

/**
* The message template for the verification SMS sent to the user upon sign up.
* See https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-templates.html to
* learn more about message templates.
* @default 'The verification code to your new account is {####}'
*/
readonly smsMessage?: string;
}

/**
* User pool configuration when administrators sign users up.
*/
export interface UserInvitationConfig {
/**
* The template to the email subject that is sent to the user when an administrator signs them up to the user pool.
* @default 'Your temporary password'
*/
readonly emailSubject?: string;

/**
* The template to the email body that is sent to the user when an administrator signs them up to the user pool.
* @default 'Your username is {username} and temporary password is {####}.'
*/
readonly emailBody?: string;

/**
* The template to the SMS message that is sent to the user when an administrator signs them up to the user pool.
* @default 'Your username is {username} and temporary password is {####}'
*/
readonly smsMessage?: string;
}

/**
* Props for the UserPool construct
*/
export interface UserPoolProps {
/**
* Name of the user pool
Expand All @@ -220,6 +292,40 @@ export interface UserPoolProps {
*/
readonly userPoolName?: string;

/**
* Whether self sign up should be enabled. This can be further configured via the `selfSignUp` property.
* @default false
*/
readonly selfSignUpEnabled?: boolean;

/**
* Configuration around users signing themselves up to the user pool.
* Enable or disable self sign-up via the `selfSignUpEnabled` property.
* @default - see defaults in UserVerificationConfig
*/
readonly userVerification?: UserVerificationConfig;

/**
* Configuration around admins signing up users into a user pool.
* @default - see defaults in UserInvitationConfig
*/
readonly userInvitation?: UserInvitationConfig;

/**
* The IAM role that Cognito will assume while sending SMS messages.
* @default - a new IAM role is created
*/
readonly smsRole?: IRole;

/**
* The 'ExternalId' that Cognito service must using when assuming the `smsRole`, if the role is restricted with an 'sts:ExternalId' conditional.
* Learn more about ExternalId here - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html
*
* This property will be ignored if `smsRole` is not specified.
* @default - No external id will be configured
*/
readonly smsRoleExternalId?: string;

/**
* Method used for user registration & sign in.
* Allows either username with aliases OR sign in with email, phone, or both.
Expand Down Expand Up @@ -400,12 +506,47 @@ export class UserPool extends Resource implements IUserPool {
}
}

const emailVerificationSubject = props.userVerification?.emailSubject ?? 'Verify your new account';
const emailVerificationMessage = props.userVerification?.emailBody ?? 'Hello {username}, Your verification code is {####}';
const smsVerificationMessage = props.userVerification?.smsMessage ?? 'The verification code to your new account is {####}';

const defaultEmailOption = props.userVerification?.emailStyle ?? VerificationEmailStyle.CODE;
const verificationMessageTemplate: CfnUserPool.VerificationMessageTemplateProperty =
(defaultEmailOption === VerificationEmailStyle.CODE) ? {
defaultEmailOption,
emailMessage: emailVerificationMessage,
emailSubject: emailVerificationSubject,
smsMessage: smsVerificationMessage,
} : {
defaultEmailOption,
emailMessageByLink: emailVerificationMessage,
emailSubjectByLink: emailVerificationSubject,
smsMessage: smsVerificationMessage
};

const inviteMessageTemplate: CfnUserPool.InviteMessageTemplateProperty = {
emailMessage: props.userInvitation?.emailBody,
emailSubject: props.userInvitation?.emailSubject,
smsMessage: props.userInvitation?.smsMessage,
};
const selfSignUpEnabled = props.selfSignUpEnabled !== undefined ? props.selfSignUpEnabled : false;
const adminCreateUserConfig: CfnUserPool.AdminCreateUserConfigProperty = {
allowAdminCreateUserOnly: !selfSignUpEnabled,
inviteMessageTemplate: props.userInvitation !== undefined ? inviteMessageTemplate : undefined,
};

const userPool = new CfnUserPool(this, 'Resource', {
userPoolName: props.userPoolName,
usernameAttributes,
aliasAttributes,
autoVerifiedAttributes: props.autoVerifiedAttributes,
lambdaConfig: Lazy.anyValue({ produce: () => this.triggers })
lambdaConfig: Lazy.anyValue({ produce: () => this.triggers }),
smsConfiguration: this.smsConfiguration(props),
adminCreateUserConfig,
emailVerificationMessage,
emailVerificationSubject,
smsVerificationMessage,
verificationMessageTemplate,
});

this.userPoolId = userPool.ref;
Expand Down Expand Up @@ -528,8 +669,45 @@ export class UserPool extends Resource implements IUserPool {
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'),
principal: new ServicePrincipal('cognito-idp.amazonaws.com'),
sourceArn: this.userPoolArn
});
}

private smsConfiguration(props: UserPoolProps): CfnUserPool.SmsConfigurationProperty {
if (props.smsRole) {
return {
snsCallerArn: props.smsRole.roleArn,
externalId: props.smsRoleExternalId
};
} else {
const smsRoleExternalId = this.node.uniqueId.substr(0, 1223); // sts:ExternalId max length of 1224
const smsRole = props.smsRole ?? new Role(this, 'smsRole', {
assumedBy: new ServicePrincipal('cognito-idp.amazonaws.com', {
conditions: {
StringEquals: { 'sts:ExternalId': smsRoleExternalId }
}
}),
inlinePolicies: {
/*
* The UserPool is very particular that it must contain an 'sns:Publish' action as an inline policy.
* Ideally, a conditional that restricts this action to 'sms' protocol needs to be attached, but the UserPool deployment fails validation.
* Seems like a case of being excessively strict.
*/
'sns-publish': new PolicyDocument({
statements: [
new PolicyStatement({
actions: [ 'sns:Publish' ],
resources: [ '*' ],
})
]
})
}
});
return {
externalId: smsRoleExternalId,
snsCallerArn: smsRole.roleArn
};
}
}
}
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-cognito/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@aws-cdk/assert": "1.24.0",
"@types/nodeunit": "^0.0.30",
"cdk-build-tools": "1.24.0",
"cdk-integ-tools": "1.24.0",
"cfn2ts": "1.24.0",
"jest": "^24.9.0",
"nodeunit": "^0.11.3",
Expand Down Expand Up @@ -104,10 +105,9 @@
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientClientSecret",
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientId",
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientName",
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolProps",
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolAttributes",
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolClientProps"
]
},
"stability": "experimental"
}
}
Loading

0 comments on commit a3d69dc

Please sign in to comment.