packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts (770 lines of code) (raw):
import { Construct } from 'constructs';
import { toASCII as punycodeEncode } from 'punycode/';
import { CfnUserPool } from './cognito.generated';
import { StandardAttributeNames } from './private/attr-names';
import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr';
import { UserPoolClient, UserPoolClientOptions } from './user-pool-client';
import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain';
import { UserPoolEmail, UserPoolEmailConfig } from './user-pool-email';
import { UserPoolGroup, UserPoolGroupOptions } from './user-pool-group';
import { IUserPoolIdentityProvider } from './user-pool-idp';
import { UserPoolResourceServer, UserPoolResourceServerOptions } from './user-pool-resource-server';
import { Grant, IGrantable, IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '../../aws-iam';
import { IKey } from '../../aws-kms';
import * as lambda from '../../aws-lambda';
import { ArnFormat, Duration, IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token } from '../../core';
import { ValidationError } from '../../core/lib/errors';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
/**
* The different ways in which users of this pool can sign up or sign in.
*/
export interface SignInAliases {
/**
* Whether user is allowed to sign up or sign in with a username
* @default true
*/
readonly username?: boolean;
/**
* Whether a user is allowed to sign up or sign in with an email address
* @default false
*/
readonly email?: boolean;
/**
* Whether a user is allowed to sign up or sign in with a phone number
* @default false
*/
readonly phone?: boolean;
/**
* Whether a user is allowed to sign in with a secondary username, that can be set and modified after sign up.
* Can only be used in conjunction with `USERNAME`.
* @default false
*/
readonly preferredUsername?: boolean;
}
/**
* Attributes that can be automatically verified for users in a user pool.
*/
export interface AutoVerifiedAttrs {
/**
* Whether the email address of the user should be auto verified at sign up.
*
* Note: If both `email` and `phone` is set, Cognito only verifies the phone number. To also verify email, see here -
* https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-email-phone-verification.html
*
* @default - true, if email is turned on for `signIn`. false, otherwise.
*/
readonly email?: boolean;
/**
* Whether the phone number of the user should be auto verified at sign up.
* @default - true, if phone is turned on for `signIn`. false, otherwise.
*/
readonly phone?: boolean;
}
/**
* Attributes that will be kept until the user verifies the changed attribute.
*/
export interface KeepOriginalAttrs {
/**
* Whether the email address of the user should remain the original value until the new email address is verified.
*
* @default - false
*/
readonly email?: boolean;
/**
* Whether the phone number of the user should remain the original value until the new phone number is verified.
*
* @default - false
*/
readonly phone?: boolean;
}
/**
* Triggers for a user pool
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
*/
export interface UserPoolTriggers {
/**
* Creates an authentication challenge.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-create-auth-challenge.html
* @default - no trigger configured
*/
readonly createAuthChallenge?: lambda.IFunction;
/**
* A custom Message AWS Lambda trigger.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-message.html
* @default - no trigger configured
*/
readonly customMessage?: lambda.IFunction;
/**
* Defines the authentication challenge.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-define-auth-challenge.html
* @default - no trigger configured
*/
readonly defineAuthChallenge?: lambda.IFunction;
/**
* A post-authentication AWS Lambda trigger.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-authentication.html
* @default - no trigger configured
*/
readonly postAuthentication?: lambda.IFunction;
/**
* A post-confirmation AWS Lambda trigger.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-confirmation.html
* @default - no trigger configured
*/
readonly postConfirmation?: lambda.IFunction;
/**
* A pre-authentication AWS Lambda trigger.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-authentication.html
* @default - no trigger configured
*/
readonly 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
* @default - no trigger configured
*/
readonly preSignUp?: lambda.IFunction;
/**
* A pre-token-generation AWS Lambda trigger.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html
* @default - no trigger configured
*/
readonly preTokenGeneration?: lambda.IFunction;
/**
* A user-migration AWS Lambda trigger.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-migrate-user.html
* @default - no trigger configured
*/
readonly userMigration?: lambda.IFunction;
/**
* Verifies the authentication challenge response.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-verify-auth-challenge-response.html
* @default - no trigger configured
*/
readonly verifyAuthChallengeResponse?: lambda.IFunction;
/**
* Amazon Cognito invokes this trigger to send email notifications to users.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-email-sender.html
* @default - no trigger configured
*/
readonly customEmailSender?: lambda.IFunction;
/**
* Amazon Cognito invokes this trigger to send SMS notifications to users.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sms-sender.html
* @default - no trigger configured
*/
readonly customSmsSender?: lambda.IFunction;
/**
* Index signature.
*
* This index signature is not usable in non-TypeScript/JavaScript languages.
*
* @jsii ignore
*/
[trigger: string]: lambda.IFunction | undefined;
}
/**
* User pool operations to which lambda triggers can be attached.
*/
export class UserPoolOperation {
/**
* Creates a challenge in a custom auth flow
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-create-auth-challenge.html
*/
public static readonly CREATE_AUTH_CHALLENGE = new UserPoolOperation('createAuthChallenge');
/**
* Advanced customization and localization of messages
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-message.html
*/
public static readonly CUSTOM_MESSAGE = new UserPoolOperation('customMessage');
/**
* Determines the next challenge in a custom auth flow
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-define-auth-challenge.html
*/
public static readonly DEFINE_AUTH_CHALLENGE = new UserPoolOperation('defineAuthChallenge');
/**
* Event logging for custom analytics
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-authentication.html
*/
public static readonly POST_AUTHENTICATION = new UserPoolOperation('postAuthentication');
/**
* Custom welcome messages or event logging for custom analytics
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-post-confirmation.html
*/
public static readonly POST_CONFIRMATION = new UserPoolOperation('postConfirmation');
/**
* Custom validation to accept or deny the sign-in request
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-authentication.html
*/
public static readonly PRE_AUTHENTICATION = new UserPoolOperation('preAuthentication');
/**
* Custom validation to accept or deny the sign-up request
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html
*/
public static readonly PRE_SIGN_UP = new UserPoolOperation('preSignUp');
/**
* Add or remove attributes in Id tokens
*
* Set this parameter for legacy purposes.
* If you also set an ARN in PreTokenGenerationConfig, its value must be identical to PreTokenGeneration.
* For new instances of pre token generation triggers, set the LambdaArn of PreTokenGenerationConfig.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html
*/
public static readonly PRE_TOKEN_GENERATION = new UserPoolOperation('preTokenGeneration');
/**
* Add or remove attributes in Id tokens and Access tokens
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html
*/
public static readonly PRE_TOKEN_GENERATION_CONFIG = new UserPoolOperation('preTokenGenerationConfig');
/**
* Migrate a user from an existing user directory to user pools
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-migrate-user.html
*/
public static readonly USER_MIGRATION = new UserPoolOperation('userMigration');
/**
* Determines if a response is correct in a custom auth flow
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-verify-auth-challenge-response.html
*/
public static readonly VERIFY_AUTH_CHALLENGE_RESPONSE = new UserPoolOperation('verifyAuthChallengeResponse');
/**
* Amazon Cognito invokes this trigger to send email notifications to users.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-email-sender.html
*/
public static readonly CUSTOM_EMAIL_SENDER = new UserPoolOperation('customEmailSender');
/**
* Amazon Cognito invokes this trigger to send email notifications to users.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sms-sender.html
*/
public static readonly CUSTOM_SMS_SENDER = new UserPoolOperation('customSmsSender');
/** A custom user pool operation */
public static of(name: string): UserPoolOperation {
const lowerCamelCase = name.charAt(0).toLowerCase() + name.slice(1);
return new UserPoolOperation(lowerCamelCase);
}
/** The key to use in `CfnUserPool.LambdaConfigProperty` */
public readonly operationName: string;
private constructor(operationName: string) {
this.operationName = operationName;
}
}
/**
* The email verification style
*/
export enum VerificationEmailStyle {
/** Verify email via code */
CODE = 'CONFIRM_WITH_CODE',
/** Verify email via link */
LINK = 'CONFIRM_WITH_LINK',
}
/**
* The user pool trigger version of the request that Amazon Cognito sends to your Lambda function.
*/
export enum LambdaVersion {
/**
* V1_0 trigger
*/
V1_0 = 'V1_0',
/**
* V2_0 trigger
*
* This is supported only for PRE_TOKEN_GENERATION trigger.
*/
V2_0 = 'V2_0',
/**
* V3_0 trigger
*
* This is supported only for PRE_TOKEN_GENERATION trigger.
*/
V3_0 = 'V3_0',
}
/**
* 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 - 'The verification code to your new account is {####}' if VerificationEmailStyle.CODE is chosen,
* 'Verify your account by clicking on {##Verify Email##}' if VerificationEmailStyle.LINK is chosen.
*/
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 {####}' if VerificationEmailStyle.CODE is chosen,
* not configured if VerificationEmailStyle.LINK is chosen
*/
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;
}
/**
* 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-email-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;
/**
* The MFA token is sent to the user via EMAIL
*
* To enable email-based MFA, set `email` property to the Amazon SES email-sending configuration
* and set `feturePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`
*
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-mfa-sms-email-message.html
* @default false
*/
readonly email?: 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;
/**
* The number of previous passwords that you want Amazon Cognito to restrict each user from reusing.
*
* `passwordHistorySize` can not be set when `featurePlan` is `FeaturePlan.LITE`.
*
* @default undefined - Cognito default setting is no restriction
*/
readonly passwordHistorySize?: number;
}
/**
* Sign-in policy for User Pools.
*/
export interface SignInPolicy {
/**
* The types of authentication that you want to allow for users' first authentication prompt.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-selection-sdk.html#authentication-flows-selection-choice
*
* @default - Password only
*/
readonly allowedFirstAuthFactors?: AllowedFirstAuthFactors;
}
/**
* The types of authentication that you want to allow for users' first authentication prompt
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-selection-sdk.html#authentication-flows-selection-choice
*/
export interface AllowedFirstAuthFactors {
/**
* Whether the password authentication is allowed.
* This must be true.
*/
readonly password: boolean;
/**
* Whether the email message one-time password is allowed.
* @default false
*/
readonly emailOtp?: boolean;
/**
* Whether the SMS message one-time password is allowed.
* @default false
*/
readonly smsOtp?: boolean;
/**
* Whether the Passkey (WebAuthn) is allowed.
* @default false
*/
readonly passkey?: boolean;
}
/**
* The user-pool treatment for MFA with a passkey
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow-methods.html#amazon-cognito-user-pools-authentication-flow-methods-passkey
*/
export enum PasskeyUserVerification {
/** Passkey MFA is preferred */
PREFERRED = 'preferred',
/** Passkey MFA is required */
REQUIRED = 'required',
}
/**
* 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;
}
/**
* How will a user be able to recover their account?
*
* When a user forgets their password, they can have a code sent to their verified email or verified phone to recover their account.
* You can choose the preferred way to send codes below.
* We recommend not allowing phone to be used for both password resets and multi-factor authentication (MFA).
*
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/how-to-recover-a-user-account.html
*/
export enum AccountRecovery {
/**
* Email if available, otherwise phone, but don’t allow a user to reset their password via phone if they are also using it for MFA
*/
EMAIL_AND_PHONE_WITHOUT_MFA,
/**
* Phone if available, otherwise email, but don’t allow a user to reset their password via phone if they are also using it for MFA
*/
PHONE_WITHOUT_MFA_AND_EMAIL,
/**
* Email only
*/
EMAIL_ONLY,
/**
* Phone only, but don’t allow a user to reset their password via phone if they are also using it for MFA
*/
PHONE_ONLY_WITHOUT_MFA,
/**
* (Not Recommended) Phone if available, otherwise email, and do allow a user to reset their password via phone if they are also using it for MFA.
*/
PHONE_AND_EMAIL,
/**
* None – users will have to contact an administrator to reset their passwords
*/
NONE,
}
/**
* Device tracking settings
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html
*/
export interface DeviceTracking {
/**
* Indicates whether a challenge is required on a new device. Only applicable to a new device.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html
* @default false
*/
readonly challengeRequiredOnNewDevice: boolean;
/**
* If true, a device is only remembered on user prompt.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html
* @default false
*/
readonly deviceOnlyRememberedOnUserPrompt: boolean;
}
/**
* The different ways in which a user pool's Advanced Security Mode can be configured.
* @deprecated Advanced Security Mode is deprecated due to user pool feature plans. Use StandardThreatProtectionMode and CustomThreatProtectionMode to set Thread Protection level.
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-userpooladdons.html
*/
export enum AdvancedSecurityMode {
/** Enable advanced security mode */
ENFORCED = 'ENFORCED',
/** gather metrics on detected risks without taking action. Metrics are published to Amazon CloudWatch */
AUDIT = 'AUDIT',
/** Advanced security mode is disabled */
OFF = 'OFF',
}
/**
* The user pool feature plan, or tier.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-sign-in-feature-plans.html
*/
export enum FeaturePlan {
/** Lite feature plan */
LITE = 'LITE',
/** Essentials feature plan */
ESSENTIALS = 'ESSENTIALS',
/** Plus feature plan */
PLUS = 'PLUS',
}
/**
* The Type of Threat Protection Enabled for Standard Authentication
*
* This feature only functions if your FeaturePlan is set to FeaturePlan.PLUS
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-sign-in-feature-plans.html
*
* Acceptable values are strings with values 'ENFORCED', 'AUDIT', or 'OFF'
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-userpooladdons.html
*/
export enum StandardThreatProtectionMode {
/** Cognito automatically takes preventative actions in response to different levels of risk that you configure for your user pool */
FULL_FUNCTION = 'ENFORCED',
/** Cognito gathers metrics on detected risks, but doesn't take automatic action */
AUDIT_ONLY = 'AUDIT',
/** Cognito doesn't gather metrics on detected risks or automatically take preventative actions */
NO_ENFORCEMENT = 'OFF',
}
/**
* The Type of Threat Protection Enabled for Custom Authentication
*
* This feature only functions if your FeaturePlan is set to FeaturePlan.PLUS
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-sign-in-feature-plans.html
*
* Acceptable values are strings with values 'ENFORCED', or 'AUDIT'. For 'OFF' behavior, don't define this value
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-userpooladdons.html
*/
export enum CustomThreatProtectionMode {
/** Cognito automatically takes preventative actions in response to different levels of risk that you configure for your user pool */
FULL_FUNCTION = 'ENFORCED',
/** Cognito gathers metrics on detected risks, but doesn't take automatic action */
AUDIT_ONLY = 'AUDIT',
}
/**
* Props for the UserPool construct
*/
export interface UserPoolProps {
/**
* Name of the user pool.
*
* @default - automatically generated name by CloudFormation at deploy time.
*/
readonly userPoolName?: string;
/**
* Whether self sign-up should be enabled.
* To configure self sign-up configuration use the `userVerification` 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 be 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;
/**
* The region to integrate with SNS to send SMS messages.
*
* This property will do nothing if SMS configuration is not configured.
*
* @default - The same region as the user pool, with a few exceptions - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-sms-settings.html#user-pool-sms-settings-first-time
*/
readonly snsRegion?: string;
/**
* Setting this would explicitly enable or disable SMS role creation.
* When left unspecified, CDK will determine based on other properties if a role is needed or not.
*
* @default - CDK will determine based on other properties of the user pool if an SMS role should be created or not.
*/
readonly enableSmsRole?: boolean;
/**
* Methods in which a user registers or signs in to a user pool.
* Allows either username with aliases OR sign in with email, phone, or both.
*
* Read the sections on usernames and aliases to learn more -
* https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html
*
* To match with 'Option 1' in the above link, with a verified email, this property should be set to
* `{ username: true, email: true }`. To match with 'Option 2' in the above link with both a verified email and phone
* number, this property should be set to `{ email: true, phone: true }`.
*
* @default { username: true }
*/
readonly signInAliases?: SignInAliases;
/**
* Attributes which Cognito will look to verify automatically upon user sign up.
* EMAIL and PHONE are the only available options.
*
* @default - If `signInAlias` includes email and/or phone, they will be included in `autoVerifiedAttributes` by default.
* If absent, no attributes will be auto-verified.
*/
readonly autoVerify?: AutoVerifiedAttrs;
/**
* Attributes which Cognito will look to handle changes to the value of your users' email address and phone number attributes.
* EMAIL and PHONE are the only available options.
*
* @default - Nothing is kept.
*/
readonly keepOriginal?: KeepOriginalAttrs;
/**
* The set of attributes that are required for every user in the user pool.
* Read more on attributes here - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html
*
* @default - All standard attributes are optional and mutable.
*/
readonly standardAttributes?: StandardAttributes;
/**
* Define a set of custom attributes that can be configured for each user in the user pool.
*
* @default - No custom attributes.
*/
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;
/**
* The SMS message template sent during MFA verification.
* Use '{####}' in the template where Cognito should insert the verification code.
* @default 'Your authentication code is {####}.'
*/
readonly mfaMessage?: string;
/**
* Configure the MFA types that users can use in this user pool. Ignored if `mfa` is set to `OFF`.
*
* @default - { sms: true, otp: false, email: false }, if `mfa` is set to `OPTIONAL` or `REQUIRED`.
* { sms: false, otp: false, email:false }, otherwise
*/
readonly mfaSecondFactor?: MfaSecondFactor;
/**
* Password policy for this user pool.
* @default - see defaults on each property of PasswordPolicy.
*/
readonly passwordPolicy?: PasswordPolicy;
/**
* Sign-in policy for this user pool.
* @default - see defaults on each property of SignInPolicy.
*/
readonly signInPolicy?: SignInPolicy;
/**
* The authentication domain that passkey providers must use as a relying party (RP) in their configuration.
*
* Under the following conditions, the passkey relying party ID must be the fully-qualified domain name of your custom domain:
* - The user pool is configured for passkey authentication.
* - The user pool has a custom domain, whether or not it also has a prefix domain.
* - Your application performs authentication with managed login or the classic hosted UI.
*
* @default - No authentication domain
*/
readonly passkeyRelyingPartyId?: string;
/**
* Your user-pool treatment for MFA with a passkey.
* You can override other MFA options and require passkey MFA, or you can set it as preferred.
* When passkey MFA is preferred, the hosted UI encourages users to register a passkey at sign-in.
*
* @default - Cognito default setting is PasskeyUserVerification.PREFERRED
*/
readonly passkeyUserVerification?: PasskeyUserVerification;
/**
* Email settings for a user pool.
*
* @default - see defaults on each property of EmailSettings.
* @deprecated Use 'email' instead.
*/
readonly emailSettings?: EmailSettings;
/**
* Email settings for a user pool.
* @default - cognito will use the default email configuration
*/
readonly email?: UserPoolEmail;
/**
* Lambda functions to use for supported Cognito triggers.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
* @default - No Lambda triggers.
*/
readonly lambdaTriggers?: UserPoolTriggers;
/**
* Whether sign-in aliases should be evaluated with case sensitivity.
* For example, when this option is set to false, users will be able to sign in using either `MyUsername` or `myusername`.
* @default true
*/
readonly signInCaseSensitive?: boolean;
/**
* How will a user be able to recover their account?
*
* @default AccountRecovery.PHONE_WITHOUT_MFA_AND_EMAIL
*/
readonly accountRecovery?: AccountRecovery;
/**
* Policy to apply when the user pool is removed from the stack
*
* @default RemovalPolicy.RETAIN
*/
readonly removalPolicy?: RemovalPolicy;
/**
* Indicates whether the user pool should have deletion protection enabled.
*
* @default false
*/
readonly deletionProtection?: boolean;
/**
* Device tracking settings
* @default - see defaults on each property of DeviceTracking.
*/
readonly deviceTracking?: DeviceTracking;
/**
* This key will be used to encrypt temporary passwords and authorization codes that Amazon Cognito generates.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sender-triggers.html
* @default - no key ID configured
*/
readonly customSenderKmsKey?: IKey;
/**
* The user pool's Advanced Security Mode
* @deprecated Advanced Security Mode is deprecated due to user pool feature plans. Use StandardThreatProtectionMode and CustomThreatProtectionMode to set Thread Protection level.
* @default - no value
*/
readonly advancedSecurityMode?: AdvancedSecurityMode;
/**
* The user pool feature plan, or tier.
* This parameter determines the eligibility of the user pool for features like managed login, access-token customization, and threat protection.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-sign-in-feature-plans.html
* @default - FeaturePlan.ESSENTIALS for a newly created user pool; FeaturePlan.LITE otherwise
*/
readonly featurePlan?: FeaturePlan;
/**
* The Type of Threat Protection Enabled for Standard Authentication
*
* This feature only functions if your FeaturePlan is set to FeaturePlan.PLUS
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-sign-in-feature-plans.html
*
* Acceptable values are strings with values 'ENFORCED', 'AUDIT', or 'OFF'
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-userpooladdons.html
*
* @default - StandardThreatProtectionMode.NO_ENFORCEMENT
*/
readonly standardThreatProtectionMode?: StandardThreatProtectionMode;
/**
* The Type of Threat Protection Enabled for Custom Authentication
*
* This feature only functions if your FeaturePlan is set to FeaturePlan.PLUS
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-sign-in-feature-plans.html
*
* Acceptable values are strings with values 'ENFORCED', or 'AUDIT'. For 'OFF' behavior, don't define this value
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-userpooladdons.html
*
* @default - no value
*/
readonly customThreatProtectionMode?: CustomThreatProtectionMode;
}
/**
* Represents a Cognito UserPool
*/
export interface IUserPool extends IResource {
/**
* The physical ID of this user pool resource
* @attribute
*/
readonly userPoolId: string;
/**
* The ARN of this user pool resource
* @attribute
*/
readonly userPoolArn: string;
/**
* The provider name of this user pool resource
*
* @attribute
*/
readonly userPoolProviderName: string;
/**
* Get all identity providers registered with this user pool.
*/
readonly identityProviders: IUserPoolIdentityProvider[];
/**
* Add a new app client to this user pool.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html
*/
addClient(id: string, options?: UserPoolClientOptions): UserPoolClient;
/**
* Associate a domain to this user pool.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html
*/
addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain;
/**
* Add a new resource server to this user pool.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-resource-servers.html
*/
addResourceServer(id: string, options: UserPoolResourceServerOptions): UserPoolResourceServer;
/**
* Add a new group to this user pool.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html
*/
addGroup(id: string, options: UserPoolGroupOptions): UserPoolGroup;
/**
* Register an identity provider with this user pool.
*/
registerIdentityProvider(provider: IUserPoolIdentityProvider): void;
/**
* Adds an IAM policy statement associated with this user pool to an
* IAM principal's policy.
*/
grant(grantee: IGrantable, ...actions: string[]): Grant;
}
abstract class UserPoolBase extends Resource implements IUserPool {
public abstract readonly userPoolId: string;
public abstract readonly userPoolArn: string;
public abstract readonly userPoolProviderName: string;
public readonly identityProviders: IUserPoolIdentityProvider[] = [];
public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient {
return new UserPoolClient(this, id, {
userPool: this,
...options,
});
}
public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain {
return new UserPoolDomain(this, id, {
userPool: this,
...options,
});
}
public addResourceServer(id: string, options: UserPoolResourceServerOptions): UserPoolResourceServer {
return new UserPoolResourceServer(this, id, {
userPool: this,
...options,
});
}
public addGroup(id: string, options: UserPoolGroupOptions): UserPoolGroup {
return new UserPoolGroup(this, id, {
userPool: this,
...options,
});
}
public registerIdentityProvider(provider: IUserPoolIdentityProvider) {
this.identityProviders.push(provider);
}
public grant(grantee: IGrantable, ...actions: string[]): Grant {
return Grant.addToPrincipal({
grantee,
actions,
resourceArns: [this.userPoolArn],
scope: this,
});
}
}
/**
* Define a Cognito User Pool
*/
export class UserPool extends UserPoolBase {
/**
* Import an existing user pool based on its id.
*/
public static fromUserPoolId(scope: Construct, id: string, userPoolId: string): IUserPool {
let userPoolArn = Stack.of(scope).formatArn({
service: 'cognito-idp',
resource: 'userpool',
resourceName: userPoolId,
});
return UserPool.fromUserPoolArn(scope, id, userPoolArn);
}
/**
* Import an existing user pool based on its ARN.
*/
public static fromUserPoolArn(scope: Construct, id: string, userPoolArn: string): IUserPool {
const arnParts = Stack.of(scope).splitArn(userPoolArn, ArnFormat.SLASH_RESOURCE_NAME);
if (!arnParts.resourceName) {
throw new ValidationError('invalid user pool ARN', scope);
}
const userPoolId = arnParts.resourceName;
// ex) cognito-idp.us-east-1.amazonaws.com/us-east-1_abcdefghi
const providerName = `cognito-idp.${arnParts.region}.${Stack.of(scope).urlSuffix}/${userPoolId}`;
class ImportedUserPool extends UserPoolBase {
public readonly userPoolArn = userPoolArn;
public readonly userPoolId = userPoolId;
public readonly userPoolProviderName = providerName;
constructor() {
super(scope, id, {
account: arnParts.account,
region: arnParts.region,
});
}
}
return new ImportedUserPool();
}
/**
* 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
* @attribute
*/
public readonly userPoolProviderName: string;
/**
* User pool provider URL
* @attribute
*/
public readonly userPoolProviderUrl: string;
private triggers: CfnUserPool.LambdaConfigProperty = {};
private emailConfiguration: UserPoolEmailConfig | undefined;
constructor(scope: Construct, id: string, props: UserPoolProps = {}) {
super(scope, id);
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
const signIn = this.signInConfiguration(props);
if (props.customSenderKmsKey) {
const kmsKey = props.customSenderKmsKey;
(this.triggers as any).kmsKeyId = kmsKey.keyArn;
}
if (props.lambdaTriggers) {
for (const t of Object.keys(props.lambdaTriggers)) {
let trigger: lambda.IFunction | undefined;
switch (t) {
case 'customSmsSender':
case 'customEmailSender':
if (!this.triggers.kmsKeyId) {
throw new ValidationError('you must specify a KMS key if you are using customSmsSender or customEmailSender.', this);
}
trigger = props.lambdaTriggers[t];
const version = 'V1_0';
if (trigger !== undefined) {
this.addLambdaPermission(trigger as lambda.IFunction, t);
(this.triggers as any)[t] = {
lambdaArn: trigger.functionArn,
lambdaVersion: version,
};
}
break;
default:
trigger = props.lambdaTriggers[t] as lambda.IFunction | undefined;
if (trigger !== undefined) {
this.addLambdaPermission(trigger as lambda.IFunction, t);
(this.triggers as any)[t] = (trigger as lambda.IFunction).functionArn;
}
break;
}
}
}
const verificationMessageTemplate = this.verificationMessageConfiguration(props);
let emailVerificationMessage;
let emailVerificationSubject;
if (verificationMessageTemplate.defaultEmailOption === VerificationEmailStyle.CODE) {
emailVerificationMessage = verificationMessageTemplate.emailMessage;
emailVerificationSubject = verificationMessageTemplate.emailSubject;
}
const smsVerificationMessage = verificationMessageTemplate.smsMessage;
const inviteMessageTemplate: CfnUserPool.InviteMessageTemplateProperty = {
emailMessage: props.userInvitation?.emailBody,
emailSubject: props.userInvitation?.emailSubject,
smsMessage: props.userInvitation?.smsMessage,
};
const selfSignUpEnabled = props.selfSignUpEnabled ?? false;
const adminCreateUserConfig: CfnUserPool.AdminCreateUserConfigProperty = {
allowAdminCreateUserOnly: !selfSignUpEnabled,
inviteMessageTemplate: props.userInvitation !== undefined ? inviteMessageTemplate : undefined,
};
const passwordPolicy = this.configurePasswordPolicy(props);
const signInPolicy = this.configureSignInPolicy(props);
if (props.passkeyRelyingPartyId !== undefined && !Token.isUnresolved(props.passkeyRelyingPartyId)) {
if (props.passkeyRelyingPartyId.length < 1 || props.passkeyRelyingPartyId.length > 63) {
throw new ValidationError(`passkeyRelyingPartyId length must be (inclusively) between 1 and 63, got ${props.passkeyRelyingPartyId.length}`, this);
}
}
if (props.email && props.emailSettings) {
throw new ValidationError('you must either provide "email" or "emailSettings", but not both', this);
}
const emailConfiguration = props.email ? props.email._bind(this) : undefinedIfNoKeys({
from: encodePuny(props.emailSettings?.from),
replyToEmailAddress: encodePuny(props.emailSettings?.replyTo),
});
this.emailConfiguration = emailConfiguration;
if (
props.featurePlan !== FeaturePlan.PLUS &&
(props.advancedSecurityMode && (props.advancedSecurityMode !== AdvancedSecurityMode.OFF))
) {
throw new ValidationError('you cannot enable Advanced Security when feature plan is not Plus.', this);
}
const advancedSecurityAdditionalFlows = undefinedIfNoKeys({
customAuthMode: props.customThreatProtectionMode,
});
if (
(props.featurePlan !== FeaturePlan.PLUS) &&
(props.standardThreatProtectionMode && (props.standardThreatProtectionMode !== StandardThreatProtectionMode.NO_ENFORCEMENT) ||
advancedSecurityAdditionalFlows)
) {
throw new ValidationError('you cannot enable Threat Protection when feature plan is not Plus.', this);
}
if (
props.advancedSecurityMode &&
(props.standardThreatProtectionMode || advancedSecurityAdditionalFlows)
) {
throw new ValidationError('you cannot set Threat Protection and Advanced Security Mode at the same time. Advanced Security Mode is deprecated and should be replaced with Threat Protection instead.', this);
}
let chosenSecurityMode = props.advancedSecurityMode ?? props.standardThreatProtectionMode;
if (advancedSecurityAdditionalFlows) {
chosenSecurityMode = props.advancedSecurityMode ?? props.standardThreatProtectionMode ?? StandardThreatProtectionMode.NO_ENFORCEMENT;
}
const userPool = new CfnUserPool(this, 'Resource', {
userPoolName: props.userPoolName,
usernameAttributes: signIn.usernameAttrs,
aliasAttributes: signIn.aliasAttrs,
autoVerifiedAttributes: signIn.autoVerifyAttrs,
lambdaConfig: Lazy.any({ produce: () => undefinedIfNoKeys(this.triggers) }),
smsAuthenticationMessage: this.mfaMessage(props),
smsConfiguration: this.smsConfiguration(props),
adminCreateUserConfig,
emailVerificationMessage,
emailVerificationSubject,
smsVerificationMessage,
verificationMessageTemplate,
userPoolAddOns: undefinedIfNoKeys({
advancedSecurityAdditionalFlows: advancedSecurityAdditionalFlows,
advancedSecurityMode: chosenSecurityMode,
}),
schema: this.schemaConfiguration(props),
mfaConfiguration: props.mfa,
enabledMfas: this.mfaConfiguration(props),
policies: undefinedIfNoKeys({ passwordPolicy, signInPolicy }),
webAuthnRelyingPartyId: props.passkeyRelyingPartyId,
webAuthnUserVerification: props.passkeyUserVerification,
emailConfiguration,
usernameConfiguration: undefinedIfNoKeys({
caseSensitive: props.signInCaseSensitive,
}),
accountRecoverySetting: this.accountRecovery(props),
deviceConfiguration: props.deviceTracking,
userAttributeUpdateSettings: this.configureUserAttributeChanges(props),
userPoolTier: props.featurePlan,
deletionProtection: defaultDeletionProtection(props.deletionProtection),
});
userPool.applyRemovalPolicy(props.removalPolicy);
this.userPoolId = userPool.ref;
this.userPoolArn = userPool.attrArn;
this.userPoolProviderName = userPool.attrProviderName;
this.userPoolProviderUrl = userPool.attrProviderUrl;
}
/**
* Add a lambda trigger to a user pool operation
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
*/
@MethodMetadata()
public addTrigger(operation: UserPoolOperation, fn: lambda.IFunction, lambdaVersion?: LambdaVersion): void {
if (operation.operationName in this.triggers) {
throw new ValidationError(`A trigger for the operation ${operation.operationName} already exists.`, this);
}
if (
operation !== UserPoolOperation.PRE_TOKEN_GENERATION_CONFIG &&
lambdaVersion !== undefined &&
[LambdaVersion.V2_0, LambdaVersion.V3_0].includes(lambdaVersion)
) {
throw new ValidationError('Only the `PRE_TOKEN_GENERATION_CONFIG` operation supports V2_0 and V3_0 lambda version.', this);
}
this.addLambdaPermission(fn, operation.operationName);
switch (operation.operationName) {
case 'customEmailSender':
case 'customSmsSender':
if (!this.triggers.kmsKeyId) {
throw new ValidationError('you must specify a KMS key if you are using customSmsSender or customEmailSender.', this);
}
(this.triggers as any)[operation.operationName] = {
lambdaArn: fn.functionArn,
lambdaVersion: LambdaVersion.V1_0,
};
break;
case 'preTokenGenerationConfig':
(this.triggers as any)[operation.operationName] = {
lambdaArn: fn.functionArn,
lambdaVersion: lambdaVersion ?? LambdaVersion.V1_0,
};
break;
default:
(this.triggers as any)[operation.operationName] = fn.functionArn;
}
}
private addLambdaPermission(fn: lambda.IFunction, name: string): void {
const capitalize = name.charAt(0).toUpperCase() + name.slice(1);
fn.addPermission(`${capitalize}Cognito`, {
principal: new ServicePrincipal('cognito-idp.amazonaws.com'),
sourceArn: Lazy.string({ produce: () => this.userPoolArn }),
scope: this,
});
}
private mfaMessage(props: UserPoolProps): string | undefined {
const CODE_TEMPLATE = '{####}';
const MAX_LENGTH = 140;
const message = props.mfaMessage;
if (message && !Token.isUnresolved(message)) {
if (!message.includes(CODE_TEMPLATE)) {
throw new ValidationError(`MFA message must contain the template string '${CODE_TEMPLATE}'`, this);
}
if (message.length > MAX_LENGTH) {
throw new ValidationError(`MFA message must be between ${CODE_TEMPLATE.length} and ${MAX_LENGTH} characters`, this);
}
}
return message;
}
private verificationMessageConfiguration(props: UserPoolProps): CfnUserPool.VerificationMessageTemplateProperty {
const CODE_TEMPLATE = '{####}';
const VERIFY_EMAIL_TEMPLATE = '{##Verify Email##}';
/**
* Email message placeholder regex
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-verificationmessagetemplate.html#cfn-cognito-userpool-verificationmessagetemplate-emailmessagebylink
*/
const VERIFY_EMAIL_REGEX = /\{##[\p{L}\p{M}\p{S}\p{N}\p{P}\s*]*##\}/u;
const emailStyle = props.userVerification?.emailStyle ?? VerificationEmailStyle.CODE;
const emailSubject = props.userVerification?.emailSubject ?? 'Verify your new account';
const smsMessage = props.userVerification?.smsMessage ?? `The verification code to your new account is ${CODE_TEMPLATE}`;
if (emailStyle === VerificationEmailStyle.CODE) {
const emailMessage = props.userVerification?.emailBody ?? `The verification code to your new account is ${CODE_TEMPLATE}`;
if (!Token.isUnresolved(emailMessage) && emailMessage.indexOf(CODE_TEMPLATE) < 0) {
throw new ValidationError(`Verification email body must contain the template string '${CODE_TEMPLATE}'`, this);
}
if (!Token.isUnresolved(smsMessage) && smsMessage.indexOf(CODE_TEMPLATE) < 0) {
throw new ValidationError(`SMS message must contain the template string '${CODE_TEMPLATE}'`, this);
}
return {
defaultEmailOption: VerificationEmailStyle.CODE,
emailMessage,
emailSubject,
smsMessage,
};
} else {
const emailMessage = props.userVerification?.emailBody ??
`Verify your account by clicking on ${VERIFY_EMAIL_TEMPLATE}`;
if (!Token.isUnresolved(emailMessage) && !VERIFY_EMAIL_REGEX.test(emailMessage)) {
throw new ValidationError(`Verification email body must contain the template string '${VERIFY_EMAIL_TEMPLATE}'`, this);
}
return {
defaultEmailOption: VerificationEmailStyle.LINK,
emailMessageByLink: emailMessage,
emailSubjectByLink: emailSubject,
smsMessage,
};
}
}
private signInConfiguration(props: UserPoolProps) {
let aliasAttrs: string[] | undefined;
let usernameAttrs: string[] | undefined;
let autoVerifyAttrs: string[] | undefined;
const signIn: SignInAliases = props.signInAliases ?? { username: true };
if (signIn.preferredUsername && !signIn.username) {
throw new ValidationError('username signIn must be enabled if preferredUsername is enabled', this);
}
if (signIn.username) {
aliasAttrs = [];
if (signIn.email) { aliasAttrs.push(StandardAttributeNames.email); }
if (signIn.phone) { aliasAttrs.push(StandardAttributeNames.phoneNumber); }
if (signIn.preferredUsername) { aliasAttrs.push(StandardAttributeNames.preferredUsername); }
if (aliasAttrs.length === 0) { aliasAttrs = undefined; }
} else {
usernameAttrs = [];
if (signIn.email) { usernameAttrs.push(StandardAttributeNames.email); }
if (signIn.phone) { usernameAttrs.push(StandardAttributeNames.phoneNumber); }
}
if (props.autoVerify) {
autoVerifyAttrs = [];
if (props.autoVerify.email) { autoVerifyAttrs.push(StandardAttributeNames.email); }
if (props.autoVerify.phone) { autoVerifyAttrs.push(StandardAttributeNames.phoneNumber); }
} else if (signIn.email || signIn.phone) {
autoVerifyAttrs = [];
if (signIn.email) { autoVerifyAttrs.push(StandardAttributeNames.email); }
if (signIn.phone) { autoVerifyAttrs.push(StandardAttributeNames.phoneNumber); }
}
return { usernameAttrs, aliasAttrs, autoVerifyAttrs };
}
private smsConfiguration(props: UserPoolProps): CfnUserPool.SmsConfigurationProperty | undefined {
if (props.enableSmsRole === false && props.smsRole) {
throw new ValidationError('enableSmsRole cannot be disabled when smsRole is specified', this);
}
if (props.smsRole) {
return {
snsCallerArn: props.smsRole.roleArn,
externalId: props.smsRoleExternalId,
snsRegion: props.snsRegion,
};
}
if (props.enableSmsRole === false) {
return undefined;
}
const mfaConfiguration = this.mfaConfiguration(props);
const phoneVerification = props.signInAliases?.phone === true || props.autoVerify?.phone === true;
const roleRequired = mfaConfiguration?.includes('SMS_MFA') || phoneVerification;
if (!roleRequired && props.enableSmsRole === undefined) {
return undefined;
}
const smsRoleExternalId = Names.uniqueId(this).slice(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,
snsRegion: props.snsRegion,
};
}
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');
} if (props.mfaSecondFactor!.email) {
this.validateEmailMfa(props);
enabledMfas.push('EMAIL_OTP');
}
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 ValidationError(`tempPasswordValidity cannot be greater than 365 days (received: ${tempPasswordValidity.toDays()})`, this);
}
const minLength = props.passwordPolicy ? props.passwordPolicy.minLength ?? 8 : undefined;
if (minLength !== undefined && (minLength < 6 || minLength > 99)) {
throw new ValidationError(`minLength for password must be between 6 and 99 (received: ${minLength})`, this);
}
const passwordHistorySize = props.passwordPolicy?.passwordHistorySize;
if (passwordHistorySize !== undefined) {
if (props.featurePlan === FeaturePlan.LITE) {
throw new ValidationError('`passwordHistorySize` can not be set when `featurePlan` is `FeaturePlan.LITE`.', this);
}
if (passwordHistorySize < 0 || passwordHistorySize > 24) {
throw new ValidationError(`\`passwordHistorySize\` must be between 0 and 24 (received: ${passwordHistorySize}).`, this);
}
}
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,
passwordHistorySize: props.passwordPolicy?.passwordHistorySize,
});
}
private configureSignInPolicy(props: UserPoolProps): CfnUserPool.SignInPolicyProperty | undefined {
let allowedFirstAuthFactors: string[] | undefined = undefined;
if (props.signInPolicy?.allowedFirstAuthFactors) {
// As of writing, from testing, CFN deployment will fail if `PASSWORD` is not enabled.
if (!props.signInPolicy.allowedFirstAuthFactors.password) {
throw new ValidationError('The password authentication cannot be disabled.', this);
}
allowedFirstAuthFactors = [];
if (props.signInPolicy.allowedFirstAuthFactors.password) {
allowedFirstAuthFactors.push('PASSWORD');
}
if (props.signInPolicy.allowedFirstAuthFactors.emailOtp) {
allowedFirstAuthFactors.push('EMAIL_OTP');
}
if (props.signInPolicy.allowedFirstAuthFactors.smsOtp) {
allowedFirstAuthFactors.push('SMS_OTP');
}
if (props.signInPolicy.allowedFirstAuthFactors.passkey) {
allowedFirstAuthFactors.push('WEB_AUTHN');
}
}
/*
* Choice-based authentication is enabled when built allowedFirstAuthFactors contains any factor but PASSWORD.
* This check should be placed here to supply the way to disable choice-based authentication explicitly
* by specifying `allowedFirstAuthFactors: { password: true }`.
*/
const isChoiceBasedAuthenticationEnabled = allowedFirstAuthFactors?.some((auth) => auth !== 'PASSWORD');
if (isChoiceBasedAuthenticationEnabled && props.featurePlan === FeaturePlan.LITE) {
throw new ValidationError('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this);
}
return undefinedIfNoKeys({ allowedFirstAuthFactors });
}
private schemaConfiguration(props: UserPoolProps): CfnUserPool.SchemaAttributeProperty[] | undefined {
const schema: CfnUserPool.SchemaAttributeProperty[] = [];
if (props.standardAttributes) {
const stdAttributes = (Object.entries(props.standardAttributes) as Array<[keyof StandardAttributes, StandardAttribute]>)
.filter(([, attr]) => !!attr)
.map(([attrName, attr]) => ({
name: StandardAttributeNames[attrName],
mutable: attr.mutable ?? true,
required: attr.required ?? false,
}));
schema.push(...stdAttributes);
}
if (props.customAttributes) {
const customAttrs = Object.keys(props.customAttributes).map((attrName) => {
const attrConfig = props.customAttributes![attrName].bind();
const numberConstraints: CfnUserPool.NumberAttributeConstraintsProperty = {
minValue: attrConfig.numberConstraints?.min?.toString(),
maxValue: attrConfig.numberConstraints?.max?.toString(),
};
const stringConstraints: CfnUserPool.StringAttributeConstraintsProperty = {
minLength: attrConfig.stringConstraints?.minLen?.toString(),
maxLength: attrConfig.stringConstraints?.maxLen?.toString(),
};
return {
name: attrName,
attributeDataType: attrConfig.dataType,
numberAttributeConstraints: attrConfig.numberConstraints
? numberConstraints
: undefined,
stringAttributeConstraints: attrConfig.stringConstraints
? stringConstraints
: undefined,
mutable: attrConfig.mutable,
};
});
schema.push(...customAttrs);
}
if (schema.length === 0) {
return undefined;
}
return schema;
}
private accountRecovery(props: UserPoolProps): undefined | CfnUserPool.AccountRecoverySettingProperty {
const accountRecovery = props.accountRecovery ?? AccountRecovery.PHONE_WITHOUT_MFA_AND_EMAIL;
switch (accountRecovery) {
case AccountRecovery.EMAIL_AND_PHONE_WITHOUT_MFA:
return {
recoveryMechanisms: [
{ name: 'verified_email', priority: 1 },
{ name: 'verified_phone_number', priority: 2 },
],
};
case AccountRecovery.PHONE_WITHOUT_MFA_AND_EMAIL:
return {
recoveryMechanisms: [
{ name: 'verified_phone_number', priority: 1 },
{ name: 'verified_email', priority: 2 },
],
};
case AccountRecovery.EMAIL_ONLY:
return {
recoveryMechanisms: [{ name: 'verified_email', priority: 1 }],
};
case AccountRecovery.PHONE_ONLY_WITHOUT_MFA:
return {
recoveryMechanisms: [{ name: 'verified_phone_number', priority: 1 }],
};
case AccountRecovery.NONE:
return {
recoveryMechanisms: [{ name: 'admin_only', priority: 1 }],
};
case AccountRecovery.PHONE_AND_EMAIL:
return undefined;
default:
throw new ValidationError(`Unsupported AccountRecovery type - ${accountRecovery}`, this);
}
}
private configureUserAttributeChanges(props: UserPoolProps): CfnUserPool.UserAttributeUpdateSettingsProperty | undefined {
if (!props.keepOriginal) {
return undefined;
}
const attributesRequireVerificationBeforeUpdate: string[] = [];
if (props.keepOriginal.email) {
attributesRequireVerificationBeforeUpdate.push(StandardAttributeNames.email);
}
if (props.keepOriginal.phone) {
attributesRequireVerificationBeforeUpdate.push(StandardAttributeNames.phoneNumber);
}
return {
attributesRequireVerificationBeforeUpdate,
};
}
private validateEmailMfa(props: UserPoolProps) {
if (props.email === undefined || this.emailConfiguration?.emailSendingAccount !== 'DEVELOPER') {
throw new ValidationError('To enable email-based MFA, set `email` property to the Amazon SES email-sending configuration.', this);
}
if (props.featurePlan === FeaturePlan.LITE) {
throw new ValidationError('To enable email-based MFA, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this);
}
}
}
function undefinedIfNoKeys(struct: object): object | undefined {
const allUndefined = Object.values(struct).every(val => val === undefined);
return allUndefined ? undefined : struct;
}
function encodePuny(input: string | undefined): string | undefined {
return input !== undefined ? punycodeEncode(input) : input;
}
function defaultDeletionProtection(deletionProtection?: boolean): 'ACTIVE' | 'INACTIVE' | undefined {
if (deletionProtection === true) {
return 'ACTIVE';
}
if (deletionProtection === false) {
return 'INACTIVE';
}
return undefined;
}