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): user pool - MFA, password policy and email settings #6717

Merged
merged 12 commits into from
Mar 17, 2020
124 changes: 102 additions & 22 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ other AWS services.

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

## Table of Contents

- [User Pools](#user-pools)
- [Sign Up](#sign-up)
- [Sign In](#sign-in)
- [Attributes](#attributes)
- [Security](#security)
- [Multi-factor Authentication](#multi-factor-authentication-mfa)
- [Emails](#emails)
- [Import](#importing-user-pools)

## User Pools

User pools allow creating and managing your own directory of users that can sign up and sign in. They enable easy
Expand Down Expand Up @@ -138,6 +149,40 @@ new UserPool(this, 'myuserpool', {
});
```

### Attributes

Attributes represent the various properties of each user that's collected and stored in the user pool. Cognito
provides a set of standard attributes that are available for all user pools. Users are allowed to select any of these
standard attributes to be required. Users will not be able to sign up to the user pool without providing the required
attributes. Besides these, additional attributes can be further defined, and are known as custom attributes.

Learn more on [attributes in Cognito's
documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html).

The following code sample configures a user pool with two standard attributes (name and address) as required, and adds
four optional attributes.

```ts
new UserPool(this, 'myuserpool', {
// ...
requiredAttributes: {
fullname: true,
address: true,
},
customAttributes: {
'myappid': new StringAttribute({ minLen: 5, maxLen: 15 }),
'callingcode': new NumberAttribute({ min: 1, max: 3 }),
'isEmployee': new BooleanAttribute(),
'joinedOn': new DateTimeAttribute(),
},
});
```

As shown in the code snippet, there are data types that are available for custom attributes. The 'String' and 'Number'
data types allow for further constraints on their length and values, respectively.

Custom attributes cannot be marked as required.

### Security

Cognito sends various messages to its users via SMS, for different actions, ranging from account verification to
Expand All @@ -162,40 +207,75 @@ When the `smsRole` property is specified, the `smsRoleExternalId` may also be sp
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).

### Attributes

Attributes represent the various properties of each user that's collected and stored in the user pool. Cognito
provides a set of standard attributes that are available for all user pools. Users are allowed to select any of these
standard attributes to be required. Users will not be able to sign up to the user pool without providing the required
attributes. Besides these, additional attributes can be further defined, and are known as custom attributes.
#### Multi-factor Authentication (MFA)

Learn more on [attributes in Cognito's
documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html).
User pools can be configured to enable multi-factor authentication (MFA). It can either be turned off, set to optional
or made required. Setting MFA to optional means that individual users can choose to enable it.
Additionally, the MFA code can be sent either via SMS text message or via a time-based software token.
See the [documentation on MFA](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html) to
learn more.

The following code sample configures a user pool with two standard attributes (name and address) as required, and adds
four optional attributes.
The following code snippet marks MFA for the user pool as required. This means that all users are required to
configure an MFA token and use it for sign in. It also allows for the users to use both SMS based MFA, as well,
[time-based one time password
(TOTP)](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-totp.html).

```ts
new UserPool(this, 'myuserpool', {
// ...
// ...
requiredAttributes: {
fullname: true,
address: true,
mfa: Mfa.REQUIRED,
mfaSecondFactor: {
sms: true,
otp: true,
},
customAttributes: {
'myappid': new StringAttribute({ minLen: 5, maxLen: 15 }),
'callingcode': new NumberAttribute({ min: 1, max: 3 }),
'isEmployee': new BooleanAttribute(),
'joinedOn': new DateTimeAttribute(),
});
```

User pools can be configured with policies around a user's password. This includes the password length and the
character sets that they must contain.

Further to this, it can also be configured with the validity of the auto-generated temporary password. A temporary
password is generated by the user pool either when an admin signs up a user or when a password reset is requested.
The validity of this password dictates how long to give the user to use this password before expiring it.

The following code snippet configures these properties -

```ts
new UserPool(this, 'myuserpool', {
// ...
passwordPolicy: {
minLength: 12,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
tempPasswordValidity: Duration.days(3),
},
});
```

As shown in the code snippet, there are data types that are available for custom attributes. The 'String' and 'Number'
data types allow for further constraints on their length and values, respectively.
Note that, `tempPasswordValidity` can be specified only in whole days. Specifying fractional days would throw an error.

Custom attributes cannot be marked as required.
### Emails

Cognito sends emails to users in the user pool, when particular actions take place, such as welcome emails, invitation
emails, password resets, etc. The address from which these emails are sent can be configured on the user pool.
Read more about [email settings here](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-email.html).

```ts
new UserPool(this, 'myuserpool', {
// ...
emailTransmission: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
emailTransmission: {
email: {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is particularly around settings for how emails are sent. Leaving this as email can be confused with other parts of the user pools such as verification emails, invitation emails, etc.

I can change this to emailSettings if that would be clearer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe emailOption?

from: 'noreply@myawesomeapp.com',
replyTo: 'support@myawesomeapp.com',
},
});
```

By default, user pools are configured to use Cognito's built-in email capability, but it can also be configured to use
Amazon SES, however, support for Amazon SES is not available in the CDK yet. If you would like this to be implemented,
give [this issue](https://github.com/aws/aws-cdk/issues/6768) a +1. Until then, you can use the [cfn
layer](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) to configure this.

### Importing User Pools

Expand Down
175 changes: 174 additions & 1 deletion packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/core';
import { Construct, Duration, IResource, Lazy, Resource, Stack } from '@aws-cdk/core';
import { CfnUserPool } from './cognito.generated';
import { ICustomAttribute, RequiredAttributes } from './user-pool-attr';

Expand Down Expand Up @@ -187,6 +187,100 @@ export interface UserInvitationConfig {
readonly smsMessage?: string;
}

/**
* The different ways in which a user pool's MFA enforcement can be configured.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html
*/
export enum Mfa {
/** Users are not required to use MFA for sign in, and cannot configure one. */
OFF = 'OFF',
/** Users are not required to use MFA for sign in, but can configure one if they so choose to. */
OPTIONAL = 'OPTIONAL',
/** Users are required to configure an MFA, and have to use it to sign in. */
REQUIRED = 'ON',
}

/**
* The different ways in which a user pool can obtain their MFA token for sign in.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa.html
*/
export interface MfaSecondFactor {
/**
* The MFA token is sent to the user via SMS to their verified phone numbers
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-sms-text-message.html
* @default true
*/
readonly sms: boolean;

/**
* The MFA token is a time-based one time password that is generated by a hardware or software token
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-totp.html
* @default false
*/
readonly otp: boolean;
}

/**
* Password policy for User Pools.
*/
export interface PasswordPolicy {
/**
* The length of time the temporary password generated by an admin is valid.
* This must be provided as whole days, like Duration.days(3) or Duration.hours(48).
* Fractional days, such as Duration.hours(20), will generate an error.
* @default Duration.days(7)
*/
readonly tempPasswordValidity?: Duration;

/**
* Minimum length required for a user's password.
* @default 8
*/
readonly minLength?: number;

/**
* Whether the user is required to have lowercase characters in their password.
* @default true
*/
readonly requireLowercase?: boolean;

/**
* Whether the user is required to have uppercase characters in their password.
* @default true
*/
readonly requireUppercase?: boolean;

/**
* Whether the user is required to have digits in their password.
* @default true
*/
readonly requireDigits?: boolean;

/**
* Whether the user is required to have symbols in their password.
* @default true
*/
readonly requireSymbols?: boolean;
}

/**
* Email settings for the user pool.
*/
export interface EmailSettings {
/**
* The 'from' address on the emails received by the user.
* @default noreply@verificationemail.com
*/
readonly from?: string;

/**
* The 'replyTo' address on the emails received by the user as defined by IETF RFC-5322.
* When set, most email clients recognize to change 'to' line to this address when a reply is drafted.
* @default - Not set.
*/
readonly replyTo?: string;
}

/**
* Props for the UserPool construct
*/
Expand Down Expand Up @@ -271,6 +365,33 @@ export interface UserPoolProps {
*/
readonly customAttributes?: { [key: string]: ICustomAttribute };

/**
* Configure whether users of this user pool can or are required use MFA to sign in.
*
* @default Mfa.OFF
*/
readonly mfa?: Mfa;

/**
* Configure the MFA types that users can use in this user pool. Ignored if `mfa` is set to `OFF`.
*
* @default - { sms: true, oneTimePassword: false }, if `mfa` is set to `OPTIONAL` or `REQUIRED`.
* { sms: false, oneTimePassword: false }, otherwise
*/
readonly mfaSecondFactor?: MfaSecondFactor;

/**
* Password policy for this user pool.
* @default - see defaults on each property of PasswordPolicy.
*/
readonly passwordPolicy?: PasswordPolicy;

/**
* Email settings for a user pool.
* @default - see defaults on each property of EmailSettings.
*/
readonly emailSettings?: EmailSettings;

/**
* Lambda functions to use for supported Cognito triggers.
*
Expand Down Expand Up @@ -394,6 +515,8 @@ export class UserPool extends Resource implements IUserPool {
inviteMessageTemplate: props.userInvitation !== undefined ? inviteMessageTemplate : undefined,
};

const passwordPolicy = this.configurePasswordPolicy(props);

const userPool = new CfnUserPool(this, 'Resource', {
userPoolName: props.userPoolName,
usernameAttributes: signIn.usernameAttrs,
Expand All @@ -407,6 +530,13 @@ export class UserPool extends Resource implements IUserPool {
smsVerificationMessage,
verificationMessageTemplate,
schema: this.schemaConfiguration(props),
mfaConfiguration: props.mfa,
enabledMfas: this.mfaConfiguration(props),
policies: passwordPolicy !== undefined ? { passwordPolicy } : undefined,
emailConfiguration: undefinedIfNoKeys({
from: props.emailSettings?.from,
replyToEmailAddress: props.emailSettings?.replyTo,
}),
});

this.userPoolId = userPool.ref;
Expand Down Expand Up @@ -607,6 +737,44 @@ export class UserPool extends Resource implements IUserPool {
}
}

private mfaConfiguration(props: UserPoolProps): string[] | undefined {
if (props.mfa === undefined || props.mfa === Mfa.OFF) {
// since default is OFF, treat undefined and OFF the same way
return undefined;
} else if (props.mfaSecondFactor === undefined &&
(props.mfa === Mfa.OPTIONAL || props.mfa === Mfa.REQUIRED)) {
return [ 'SMS_MFA' ];
} else {
const enabledMfas = [];
if (props.mfaSecondFactor!.sms) {
enabledMfas.push('SMS_MFA');
}
if (props.mfaSecondFactor!.otp) {
enabledMfas.push('SOFTWARE_TOKEN_MFA');
}
return enabledMfas;
}
}

private configurePasswordPolicy(props: UserPoolProps): CfnUserPool.PasswordPolicyProperty | undefined {
const tempPasswordValidity = props.passwordPolicy?.tempPasswordValidity;
if (tempPasswordValidity !== undefined && tempPasswordValidity.toDays() > Duration.days(365).toDays()) {
throw new Error(`tempPasswordValidity cannot be greater than 365 days (received: ${tempPasswordValidity.toDays()})`);
}
const minLength = props.passwordPolicy?.minLength;
if (minLength !== undefined && (minLength < 6 || minLength > 99)) {
throw new Error(`minLength for password must be between 6 and 99 (received: ${minLength})`);
}
return undefinedIfNoKeys({
temporaryPasswordValidityDays: tempPasswordValidity?.toDays({ integral: true }),
minimumLength: minLength,
requireLowercase: props.passwordPolicy?.requireLowercase,
requireUppercase: props.passwordPolicy?.requireUppercase,
requireNumbers: props.passwordPolicy?.requireDigits,
requireSymbols: props.passwordPolicy?.requireSymbols,
});
}

private schemaConfiguration(props: UserPoolProps): CfnUserPool.SchemaAttributeProperty[] | undefined {
const schema: CfnUserPool.SchemaAttributeProperty[] = [];

Expand Down Expand Up @@ -683,4 +851,9 @@ const enum StandardAttribute {
TIMEZONE = 'zoneinfo',
LAST_UPDATE_TIME = 'updated_at',
WEBSITE = 'website',
}

function undefinedIfNoKeys(struct: object): object | undefined {
const allUndefined = Object.values(struct).reduce((acc, v) => acc && (v === undefined), true);
return allUndefined ? undefined : struct;
}
Loading