From d3e94dfe89ce0ccc45cad3ebb971b190abd0d4b1 Mon Sep 17 00:00:00 2001 From: Tietew Date: Tue, 3 Dec 2024 15:35:17 +0900 Subject: [PATCH 01/19] feat: passwordless sign-in --- packages/aws-cdk-lib/aws-cognito/README.md | 53 +++++++++++ .../aws-cdk-lib/aws-cognito/lib/user-pool.ts | 88 ++++++++++++++++++- .../aws-cognito/test/user-pool.test.ts | 74 +++++++++++++++- 3 files changed, 213 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/README.md b/packages/aws-cdk-lib/aws-cognito/README.md index e59370ef2d7cd..e3739f6ac1c23 100644 --- a/packages/aws-cdk-lib/aws-cognito/README.md +++ b/packages/aws-cdk-lib/aws-cognito/README.md @@ -22,6 +22,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Code Verification](#code-verification) - [Link Verification](#link-verification) - [Sign In](#sign-in) + - [Passwordless sign-in](#passwordless-sign-in) - [Attributes](#attributes) - [Attribute verification](#attribute-verification) - [Security](#security) @@ -193,6 +194,58 @@ new cognito.UserPool(this, 'myuserpool', { A user pool can optionally ignore case when evaluating sign-ins. When `signInCaseSensitive` is false, Cognito will not check the capitalization of the alias when signing in. Default is true. +#### Passwordless sign-in + +User pools can be configured to allow passwordless sign-in with email message one-time password, SMS message one-time password, and passkey (WebAuthn) sign-in. Passwordless sign-in requires the [Essentials feature plan](#user-pool-feature-plans). + +For details of authentication methods and client implementation, see [Manage authentication methods in AWS SDKs](https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-selection-sdk.html). + +The following code configures a user pool with passwordless sign-in enabled: + +```ts +new cognito.UserPool(this, 'myuserpool', { + allowedFirstAuthFactors: { + emailOtp: true, // enables email message one-time password + smsOtp: true, // enables SMS message one-time password + passkey: true, // enables passkey sign-in + }, +}); +``` + +⚠️ enabling SMS message one-time password requires the AWS account be activated to SMS message sending. +For details, see [SMS message settings for Amazon Cognito user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-sms-settings.html). + +When enabling passkey sign-in, you should specify the authentication domain used as the relying party ID. +Learn more about [passkey sign-in of user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow-methods.html#amazon-cognito-user-pools-authentication-flow-methods-passkey) and [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API). + +To use the hosted Amazon Cognito domain as the relying party ID: + +```ts +new cognito.UserPool(this, 'myuserpool', { + allowedFirstAuthFactors: { passkey: true }, + passkeyRelyingPartyId: 'myclientname.auth.region-name.amazoncognito.com', +}); +``` + +To use the custom domain as the relying party ID: + +```ts +new cognito.UserPool(this, 'myuserpool', { + allowedFirstAuthFactors: { passkey: true }, + passkeyRelyingPartyId: 'auth.example.com', +}); +``` + +You can also configure the passkey is required (preferred by default): + +```ts +new cognito.UserPool(this, 'myuserpool', { + allowedFirstAuthFactors: { passkey: true }, + passkeyRelyingPartyId: 'auth.example.com', + passkeyVerification: cognito.PasskeyVerification.REQUIRED, +}); +``` + ### Attributes Attributes represent the various properties of each user that's collected and stored in the user pool. Cognito diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index 9ba99724cdb6e..dee3dbbfbfe3e 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -459,6 +459,39 @@ export interface PasswordPolicy { readonly requireSymbols?: boolean; } +/** + * 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 AuthFactor { + /** + * 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 PasskeyVerification { + /** Passkey MFA is preferred */ + PREFERRED = 'preferred', + /** Passkey MFA is required */ + REQUIRED = 'required', +}; + /** * Email settings for the user pool. */ @@ -694,6 +727,36 @@ export interface UserPoolProps { */ readonly passwordPolicy?: PasswordPolicy; + /** + * The types of authentication that you want to allow for users' first authentication prompt. + * The password authentication is allowed always. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-selection-sdk.html#authentication-flows-selection-choice + * + * @default - Password only + */ + readonly allowedFirstAuthFactors?: AuthFactor; + + /** + * 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 PasskeyVerification.PREFERRED + */ + readonly passkeyVerification?: PasskeyVerification; + /** * Email settings for a user pool. * @@ -1007,6 +1070,7 @@ export class UserPool extends UserPoolBase { }; const passwordPolicy = this.configurePasswordPolicy(props); + const signInPolicy = this.configureSignInPolicy(props); if (props.email && props.emailSettings) { throw new Error('you must either provide "email" or "emailSettings", but not both'); @@ -1036,7 +1100,9 @@ export class UserPool extends UserPoolBase { schema: this.schemaConfiguration(props), mfaConfiguration: props.mfa, enabledMfas: this.mfaConfiguration(props), - policies: passwordPolicy !== undefined ? { passwordPolicy } : undefined, + policies: undefinedIfNoKeys({ passwordPolicy, signInPolicy }), + webAuthnRelyingPartyId: props.passkeyRelyingPartyId, + webAuthnUserVerification: props.passkeyVerification, emailConfiguration, usernameConfiguration: undefinedIfNoKeys({ caseSensitive: props.signInCaseSensitive, @@ -1291,6 +1357,26 @@ export class UserPool extends UserPoolBase { }); } + private configureSignInPolicy(props: UserPoolProps): CfnUserPool.SignInPolicyProperty | undefined { + if (!props.allowedFirstAuthFactors) { + return undefined; + } + + // TODO: validate whether the feature plan is not Lite + + const allowedFirstAuthFactors = ['PASSWORD']; + if (props.allowedFirstAuthFactors.emailOtp) { + allowedFirstAuthFactors.push('EMAIL_OTP'); + } + if (props.allowedFirstAuthFactors.smsOtp) { + allowedFirstAuthFactors.push('SMS_OTP'); + } + if (props.allowedFirstAuthFactors.passkey) { + allowedFirstAuthFactors.push('WEB_AUTHN'); + } + return { allowedFirstAuthFactors }; + } + private schemaConfiguration(props: UserPoolProps): CfnUserPool.SchemaAttributeProperty[] | undefined { const schema: CfnUserPool.SchemaAttributeProperty[] = []; diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index bcf69ec7948e9..d65b45626c3b1 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -5,7 +5,7 @@ import { Role, ServicePrincipal } from '../../aws-iam'; import * as kms from '../../aws-kms'; import * as lambda from '../../aws-lambda'; import { CfnParameter, Duration, Stack, Tags } from '../../core'; -import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, UserPoolEmail, AdvancedSecurityMode, LambdaVersion } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, UserPoolEmail, AdvancedSecurityMode, LambdaVersion, PasskeyVerification } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -2088,6 +2088,78 @@ describe('User Pool', () => { }), })).toThrow(/"fromEmail" contains a different domain than the "sesVerifiedDomain"/); }); + + test('allowFirstAuthFactors is not present if option is not provided', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', {}); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + Policies: Match.absent(), + }); + }); + + test.each([ + ['blank', {}, ['PASSWORD']], + ['email OTP', { emailOtp: true }, ['PASSWORD', 'EMAIL_OTP']], + ['SMS OTP', { smsOtp: true }, ['PASSWORD', 'SMS_OTP']], + ['passkey', { passkey: true }, ['PASSWORD', 'WEB_AUTHN']], + ['email OTP and SMS OTP', { emailOtp: true, smsOtp: true }, ['PASSWORD', 'EMAIL_OTP', 'SMS_OTP']], + ['email OTP and passkey', { emailOtp: true, passkey: true }, ['PASSWORD', 'EMAIL_OTP', 'WEB_AUTHN']], + ['SMS OTP and passkey', { smsOtp: true, passkey: true }, ['PASSWORD', 'SMS_OTP', 'WEB_AUTHN']], + ['all enabled', { emailOtp: true, smsOtp: true, passkey: true }, ['PASSWORD', 'EMAIL_OTP', 'SMS_OTP', 'WEB_AUTHN']], + ])('allowFirstAuthFactors is configured correctly when set to %s', (_, allowedFirstAuthFactors, compareArray) => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { allowedFirstAuthFactors }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + Policies: { + SignInPolicy: { + AllowedFirstAuthFactors: compareArray, + }, + }, + }); + }); + + // TODO: test('allowFirstAuthFactors throws when the feature plan is Lite') + + test('passkeyRelyingPartyId is configured', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + passkeyRelyingPartyId: 'example.com', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + WebAuthnRelyingPartyID: 'example.com', + }); + }); + + test.each([ + [PasskeyVerification.PREFERRED, 'preferred'], + [PasskeyVerification.REQUIRED, 'required'], + ])('passkeyVerification is configured correctly when set to (%s)', (passkeyVerification, compareString) => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { passkeyVerification }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + WebAuthnUserVerification: compareString, + }); + }); }); test('device tracking is configured correctly', () => { From 9a5a2304c3527b1286b0e56bcfb5d9561eec32b5 Mon Sep 17 00:00:00 2001 From: Tietew Date: Tue, 3 Dec 2024 15:37:29 +0900 Subject: [PATCH 02/19] integ test --- ...efaultTestDeployAssertE3E7D2A4.assets.json | 19 ++ ...aultTestDeployAssertE3E7D2A4.template.json | 36 ++++ .../cdk.out | 1 + .../integ-user-pool-passwordless.assets.json | 19 ++ ...integ-user-pool-passwordless.template.json | 88 +++++++++ .../integ.json | 12 ++ .../manifest.json | 119 +++++++++++++ .../tree.json | 168 ++++++++++++++++++ .../test/integ.user-pool-passwordless.ts | 20 +++ 9 files changed, 482 insertions(+) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json new file mode 100644 index 0000000000000..8bbe077289ad2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json @@ -0,0 +1,19 @@ +{ + "version": "38.0.1", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "IntegTestDefaultTestDeployAssertE3E7D2A4.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/cdk.out new file mode 100644 index 0000000000000..c6e612584e352 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"38.0.1"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.assets.json new file mode 100644 index 0000000000000..557ed52dc5dbc --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.assets.json @@ -0,0 +1,19 @@ +{ + "version": "38.0.1", + "files": { + "e8fd9461c2a642a58e740da0d66211db2c1e638fb438602d9cd589f69d8df18e": { + "source": { + "path": "integ-user-pool-passwordless.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "e8fd9461c2a642a58e740da0d66211db2c1e638fb438602d9cd589f69d8df18e.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.template.json new file mode 100644 index 0000000000000..ef5fecb5b81b0 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.template.json @@ -0,0 +1,88 @@ +{ + "Resources": { + "myuserpool01998219": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "DeletionProtection": "INACTIVE", + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "Policies": { + "SignInPolicy": { + "AllowedFirstAuthFactors": [ + "PASSWORD", + "EMAIL_OTP", + "WEB_AUTHN" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + }, + "WebAuthnRelyingPartyID": "example.com", + "WebAuthnUserVerification": "required" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Outputs": { + "userpoolpasswordless": { + "Value": { + "Ref": "myuserpool01998219" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ.json new file mode 100644 index 0000000000000..1b47987489bd1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "38.0.1", + "testCases": { + "IntegTest/DefaultTest": { + "stacks": [ + "integ-user-pool-passwordless" + ], + "assertionStack": "IntegTest/DefaultTest/DeployAssert", + "assertionStackName": "IntegTestDefaultTestDeployAssertE3E7D2A4" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/manifest.json new file mode 100644 index 0000000000000..3df9c0b283e4b --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/manifest.json @@ -0,0 +1,119 @@ +{ + "version": "38.0.1", + "artifacts": { + "integ-user-pool-passwordless.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-user-pool-passwordless.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-user-pool-passwordless": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-user-pool-passwordless.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/e8fd9461c2a642a58e740da0d66211db2c1e638fb438602d9cd589f69d8df18e.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-user-pool-passwordless.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integ-user-pool-passwordless.assets" + ], + "metadata": { + "/integ-user-pool-passwordless/myuserpool/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "myuserpool01998219" + } + ], + "/integ-user-pool-passwordless/user-pool-passwordless": [ + { + "type": "aws:cdk:logicalId", + "data": "userpoolpasswordless" + } + ], + "/integ-user-pool-passwordless/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-user-pool-passwordless/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-user-pool-passwordless" + }, + "IntegTestDefaultTestDeployAssertE3E7D2A4.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "IntegTestDefaultTestDeployAssertE3E7D2A4": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "IntegTestDefaultTestDeployAssertE3E7D2A4.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "IntegTestDefaultTestDeployAssertE3E7D2A4.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "IntegTestDefaultTestDeployAssertE3E7D2A4.assets" + ], + "metadata": { + "/IntegTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/IntegTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "IntegTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/tree.json new file mode 100644 index 0000000000000..6a8f3dba7d17a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/tree.json @@ -0,0 +1,168 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "integ-user-pool-passwordless": { + "id": "integ-user-pool-passwordless", + "path": "integ-user-pool-passwordless", + "children": { + "myuserpool": { + "id": "myuserpool", + "path": "integ-user-pool-passwordless/myuserpool", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-passwordless/myuserpool/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Cognito::UserPool", + "aws:cdk:cloudformation:props": { + "accountRecoverySetting": { + "recoveryMechanisms": [ + { + "name": "verified_phone_number", + "priority": 1 + }, + { + "name": "verified_email", + "priority": 2 + } + ] + }, + "adminCreateUserConfig": { + "allowAdminCreateUserOnly": true + }, + "deletionProtection": "INACTIVE", + "emailVerificationMessage": "The verification code to your new account is {####}", + "emailVerificationSubject": "Verify your new account", + "policies": { + "signInPolicy": { + "allowedFirstAuthFactors": [ + "PASSWORD", + "EMAIL_OTP", + "WEB_AUTHN" + ] + } + }, + "smsVerificationMessage": "The verification code to your new account is {####}", + "verificationMessageTemplate": { + "defaultEmailOption": "CONFIRM_WITH_CODE", + "emailMessage": "The verification code to your new account is {####}", + "emailSubject": "Verify your new account", + "smsMessage": "The verification code to your new account is {####}" + }, + "webAuthnRelyingPartyId": "example.com", + "webAuthnUserVerification": "required" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cognito.CfnUserPool", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cognito.UserPool", + "version": "0.0.0" + } + }, + "user-pool-passwordless": { + "id": "user-pool-passwordless", + "path": "integ-user-pool-passwordless/user-pool-passwordless", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnOutput", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-user-pool-passwordless/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-user-pool-passwordless/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "IntegTest": { + "id": "IntegTest", + "path": "IntegTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "IntegTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "IntegTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "IntegTest/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "IntegTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "IntegTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts new file mode 100644 index 0000000000000..8be8a3a999f5a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts @@ -0,0 +1,20 @@ +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import { App, CfnOutput, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { PasskeyVerification, UserPool } from 'aws-cdk-lib/aws-cognito'; + +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-passwordless'); + +const userpool = new UserPool(stack, 'myuserpool', { + allowedFirstAuthFactors: { emailOtp: true, passkey: true }, + passkeyRelyingPartyId: 'example.com', + passkeyVerification: PasskeyVerification.REQUIRED, + removalPolicy: RemovalPolicy.DESTROY, + deletionProtection: false, +}); + +new CfnOutput(stack, 'user-pool-passwordless', { + value: userpool.userPoolId, +}); + +new IntegTest(app, 'IntegTest', { testCases: [stack] }); From a6f16d89c3d895574a833876c5e55aac9bca3e08 Mon Sep 17 00:00:00 2001 From: Tietew Date: Thu, 5 Dec 2024 11:10:03 +0900 Subject: [PATCH 03/19] check featurePlan --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 4 +++- .../aws-cdk-lib/aws-cognito/test/user-pool.test.ts | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index dee3dbbfbfe3e..cea420ece7d3a 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -1362,7 +1362,9 @@ export class UserPool extends UserPoolBase { return undefined; } - // TODO: validate whether the feature plan is not Lite + if (props.featurePlan === FeaturePlan.LITE) { + throw new Error('To enable passwordless sign-in, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); + } const allowedFirstAuthFactors = ['PASSWORD']; if (props.allowedFirstAuthFactors.emailOtp) { diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index d65b45626c3b1..f7677a6c33813 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2128,7 +2128,18 @@ describe('User Pool', () => { }); }); - // TODO: test('allowFirstAuthFactors throws when the feature plan is Lite') + test('allowFirstAuthFactors throws when the feature plan is Lite', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => { + new UserPool(stack, 'Pool', { + allowedFirstAuthFactors: { emailOtp: true }, + featurePlan: FeaturePlan.LITE, + }); + }).toThrow('To enable passwordless sign-in, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); + }); test('passkeyRelyingPartyId is configured', () => { // GIVEN From 3ce032fd5c943418d263c3a6cfb2fcdb6c639159 Mon Sep 17 00:00:00 2001 From: Tietew Date: Fri, 6 Dec 2024 12:55:28 +0900 Subject: [PATCH 04/19] emphasize *user* verification --- .../test/integ.user-pool-passwordless.ts | 4 +-- .../aws-cdk-lib/aws-cognito/lib/user-pool.ts | 8 ++--- .../aws-cognito/test/user-pool.test.ts | 36 ++++++++++--------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts index 8be8a3a999f5a..5abdc5839ab0a 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts @@ -1,6 +1,6 @@ import { IntegTest } from '@aws-cdk/integ-tests-alpha'; import { App, CfnOutput, RemovalPolicy, Stack } from 'aws-cdk-lib'; -import { PasskeyVerification, UserPool } from 'aws-cdk-lib/aws-cognito'; +import { PasskeyUserVerification, UserPool } from 'aws-cdk-lib/aws-cognito'; const app = new App(); const stack = new Stack(app, 'integ-user-pool-passwordless'); @@ -8,7 +8,7 @@ const stack = new Stack(app, 'integ-user-pool-passwordless'); const userpool = new UserPool(stack, 'myuserpool', { allowedFirstAuthFactors: { emailOtp: true, passkey: true }, passkeyRelyingPartyId: 'example.com', - passkeyVerification: PasskeyVerification.REQUIRED, + passkeyUserVerification: PasskeyUserVerification.REQUIRED, removalPolicy: RemovalPolicy.DESTROY, deletionProtection: false, }); diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index cea420ece7d3a..6d80099eb1eb6 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -485,7 +485,7 @@ export interface AuthFactor { * 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 PasskeyVerification { +export enum PasskeyUserVerification { /** Passkey MFA is preferred */ PREFERRED = 'preferred', /** Passkey MFA is required */ @@ -753,9 +753,9 @@ export interface UserPoolProps { * 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 PasskeyVerification.PREFERRED + * @default PasskeyUserVerification.PREFERRED */ - readonly passkeyVerification?: PasskeyVerification; + readonly passkeyUserVerification?: PasskeyUserVerification; /** * Email settings for a user pool. @@ -1102,7 +1102,7 @@ export class UserPool extends UserPoolBase { enabledMfas: this.mfaConfiguration(props), policies: undefinedIfNoKeys({ passwordPolicy, signInPolicy }), webAuthnRelyingPartyId: props.passkeyRelyingPartyId, - webAuthnUserVerification: props.passkeyVerification, + webAuthnUserVerification: props.passkeyUserVerification, emailConfiguration, usernameConfiguration: undefinedIfNoKeys({ caseSensitive: props.signInCaseSensitive, diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index f7677a6c33813..10c4e787fb726 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -5,7 +5,7 @@ import { Role, ServicePrincipal } from '../../aws-iam'; import * as kms from '../../aws-kms'; import * as lambda from '../../aws-lambda'; import { CfnParameter, Duration, Stack, Tags } from '../../core'; -import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, UserPoolEmail, AdvancedSecurityMode, LambdaVersion, PasskeyVerification } from '../lib'; +import { AccountRecovery, Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle, UserPoolEmail, AdvancedSecurityMode, LambdaVersion, PasskeyUserVerification } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -2102,27 +2102,29 @@ describe('User Pool', () => { }); }); - test.each([ - ['blank', {}, ['PASSWORD']], - ['email OTP', { emailOtp: true }, ['PASSWORD', 'EMAIL_OTP']], - ['SMS OTP', { smsOtp: true }, ['PASSWORD', 'SMS_OTP']], - ['passkey', { passkey: true }, ['PASSWORD', 'WEB_AUTHN']], - ['email OTP and SMS OTP', { emailOtp: true, smsOtp: true }, ['PASSWORD', 'EMAIL_OTP', 'SMS_OTP']], - ['email OTP and passkey', { emailOtp: true, passkey: true }, ['PASSWORD', 'EMAIL_OTP', 'WEB_AUTHN']], - ['SMS OTP and passkey', { smsOtp: true, passkey: true }, ['PASSWORD', 'SMS_OTP', 'WEB_AUTHN']], - ['all enabled', { emailOtp: true, smsOtp: true, passkey: true }, ['PASSWORD', 'EMAIL_OTP', 'SMS_OTP', 'WEB_AUTHN']], - ])('allowFirstAuthFactors is configured correctly when set to %s', (_, allowedFirstAuthFactors, compareArray) => { + test('allowFirstAuthFactors are correctly named', () => { // GIVEN const stack = new Stack(); // WHEN - new UserPool(stack, 'Pool', { allowedFirstAuthFactors }); + new UserPool(stack, 'Pool', { + allowedFirstAuthFactors: { + emailOtp: true, + smsOtp: true, + passkey: true, + }, + }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { Policies: { SignInPolicy: { - AllowedFirstAuthFactors: compareArray, + AllowedFirstAuthFactors: [ + 'PASSWORD', + 'EMAIL_OTP', + 'SMS_OTP', + 'WEB_AUTHN', + ], }, }, }); @@ -2157,14 +2159,14 @@ describe('User Pool', () => { }); test.each([ - [PasskeyVerification.PREFERRED, 'preferred'], - [PasskeyVerification.REQUIRED, 'required'], - ])('passkeyVerification is configured correctly when set to (%s)', (passkeyVerification, compareString) => { + [PasskeyUserVerification.PREFERRED, 'preferred'], + [PasskeyUserVerification.REQUIRED, 'required'], + ])('passkeyUserVerification is configured correctly when set to (%s)', (passkeyUserVerification, compareString) => { // GIVEN const stack = new Stack(); // WHEN - new UserPool(stack, 'Pool', { passkeyVerification }); + new UserPool(stack, 'Pool', { passkeyUserVerification }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { From 53ffbfb54af8e2bfeb14252742031a423838dfc0 Mon Sep 17 00:00:00 2001 From: Tietew Date: Fri, 6 Dec 2024 12:55:48 +0900 Subject: [PATCH 05/19] update to refer choice-based authentication --- packages/aws-cdk-lib/aws-cognito/README.md | 36 +++++++++++++--------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/README.md b/packages/aws-cdk-lib/aws-cognito/README.md index e3739f6ac1c23..df7c214ac7b28 100644 --- a/packages/aws-cdk-lib/aws-cognito/README.md +++ b/packages/aws-cdk-lib/aws-cognito/README.md @@ -22,7 +22,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Code Verification](#code-verification) - [Link Verification](#link-verification) - [Sign In](#sign-in) - - [Passwordless sign-in](#passwordless-sign-in) + - [Choise-based authentication](#choice-based-authentication-passwordless-sign-in--passkey-sign-in) - [Attributes](#attributes) - [Attribute verification](#attribute-verification) - [Security](#security) @@ -194,55 +194,61 @@ new cognito.UserPool(this, 'myuserpool', { A user pool can optionally ignore case when evaluating sign-ins. When `signInCaseSensitive` is false, Cognito will not check the capitalization of the alias when signing in. Default is true. -#### Passwordless sign-in +#### Choice-based authentication: passwordless sign-in / passkey sign-in -User pools can be configured to allow passwordless sign-in with email message one-time password, SMS message one-time password, and passkey (WebAuthn) sign-in. Passwordless sign-in requires the [Essentials feature plan](#user-pool-feature-plans). +User pools can be configured to allow the following authentication methods in choice-based authentication: +- Passwordless sign-in with email message one-time password +- Passwordless sign-in with SMS message one-time password +- Passkey (WebAuthn) sign-in + +To use choice-based authentication, [User pool feature plan](#user-pool-feature-plans) should be Essentials or higher. For details of authentication methods and client implementation, see [Manage authentication methods in AWS SDKs](https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-selection-sdk.html). -The following code configures a user pool with passwordless sign-in enabled: +The following code configures a user pool with choice-based authentication enabled: ```ts -new cognito.UserPool(this, 'myuserpool', { +const userPool = new cognito.UserPool(this, 'myuserpool', { allowedFirstAuthFactors: { emailOtp: true, // enables email message one-time password smsOtp: true, // enables SMS message one-time password passkey: true, // enables passkey sign-in }, }); + +// You should also configure the user pool client to allow USER_AUTH authentication flow +userPool.addClient('myclient', { + authFlows: { user: true }, +}); ``` -⚠️ enabling SMS message one-time password requires the AWS account be activated to SMS message sending. -For details, see [SMS message settings for Amazon Cognito user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-sms-settings.html). +⚠️ Enabling SMS message one-time password requires the AWS account be activated to SMS message sending. +Learn more about [SMS message settings for Amazon Cognito user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-sms-settings.html). When enabling passkey sign-in, you should specify the authentication domain used as the relying party ID. Learn more about [passkey sign-in of user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow-methods.html#amazon-cognito-user-pools-authentication-flow-methods-passkey) and [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API). -To use the hosted Amazon Cognito domain as the relying party ID: - ```ts +// Use the hosted Amazon Cognito domain as the relying party ID new cognito.UserPool(this, 'myuserpool', { allowedFirstAuthFactors: { passkey: true }, passkeyRelyingPartyId: 'myclientname.auth.region-name.amazoncognito.com', }); -``` -To use the custom domain as the relying party ID: - -```ts +// Use the custom domain as the relying party ID new cognito.UserPool(this, 'myuserpool', { allowedFirstAuthFactors: { passkey: true }, passkeyRelyingPartyId: 'auth.example.com', }); ``` -You can also configure the passkey is required (preferred by default): +You can configure user verification to be preferred (default) or required. When you set user verification to preferred, users can set up authenticators that don't have the user verification capability, and registration and authentication operations can succeed without user verification. To mandate user verification in passkey registration and authentication, specify `passkeyUserVerification` to `PasskeyUserVerification.REQUIRED`. ```ts new cognito.UserPool(this, 'myuserpool', { allowedFirstAuthFactors: { passkey: true }, passkeyRelyingPartyId: 'auth.example.com', - passkeyVerification: cognito.PasskeyVerification.REQUIRED, + passkeyUserVerification: cognito.PasskeyUserVerification.REQUIRED, }); ``` From 6b65ce006cc02f42bd1831f2a6a90c1922dcccb4 Mon Sep 17 00:00:00 2001 From: Tietew Date: Thu, 30 Jan 2025 11:48:37 +0900 Subject: [PATCH 06/19] return undefined when no allowed factors --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 13 +++++++++---- .../aws-cdk-lib/aws-cognito/test/user-pool.test.ts | 6 ++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index c418c7d722ed6..43a99bec54757 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -1412,10 +1412,7 @@ export class UserPool extends UserPoolBase { return undefined; } - if (props.featurePlan === FeaturePlan.LITE) { - throw new Error('To enable passwordless sign-in, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); - } - + // PASSWORD should be configured as one of the allowed first auth factors. const allowedFirstAuthFactors = ['PASSWORD']; if (props.allowedFirstAuthFactors.emailOtp) { allowedFirstAuthFactors.push('EMAIL_OTP'); @@ -1426,6 +1423,14 @@ export class UserPool extends UserPoolBase { if (props.allowedFirstAuthFactors.passkey) { allowedFirstAuthFactors.push('WEB_AUTHN'); } + if (allowedFirstAuthFactors.length === 1) { + return undefined; + } + + if (props.featurePlan === FeaturePlan.LITE) { + throw new Error('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); + } + return { allowedFirstAuthFactors }; } diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index 4755595e8a2ce..bf5cbc2199bda 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2091,7 +2091,9 @@ describe('User Pool', () => { const stack = new Stack(); // WHEN - new UserPool(stack, 'Pool', {}); + new UserPool(stack, 'Pool', { + allowedFirstAuthFactors: {}, + }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { @@ -2137,7 +2139,7 @@ describe('User Pool', () => { allowedFirstAuthFactors: { emailOtp: true }, featurePlan: FeaturePlan.LITE, }); - }).toThrow('To enable passwordless sign-in, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); + }).toThrow('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); }); test('passkeyRelyingPartyId is configured', () => { From 51468c74f645a3e1091b8917eb1522bb7f3602b7 Mon Sep 17 00:00:00 2001 From: Tietew Date: Thu, 30 Jan 2025 12:02:40 +0900 Subject: [PATCH 07/19] validate length of passkeyRelyingPartyId --- .../aws-cdk-lib/aws-cognito/lib/user-pool.ts | 6 ++++++ .../aws-cognito/test/user-pool.test.ts | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index 43a99bec54757..720a8c819e7b1 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -1105,6 +1105,12 @@ export class UserPool extends UserPoolBase { 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); } diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index bf5cbc2199bda..919cfa8f2a390 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2157,6 +2157,26 @@ describe('User Pool', () => { }); }); + test('passkeyRelyingPartyId length must be >= 1', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => { + new UserPool(stack, 'Pool1', { passkeyRelyingPartyId: '' }); + }).toThrow('passkeyRelyingPartyId length must be (inclusively) between 1 and 63, got 0'); + }); + + test('passkeyRelyingPartyId length must be <= 63', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + expect(() => { + new UserPool(stack, 'Pool2', { passkeyRelyingPartyId: 'x'.repeat(64) }); + }).toThrow('passkeyRelyingPartyId length must be (inclusively) between 1 and 63, got 64'); + }); + test.each([ [PasskeyUserVerification.PREFERRED, 'preferred'], [PasskeyUserVerification.REQUIRED, 'required'], From 3c1778bf202b99c8c9b469fc65ac5c0dbb888260 Mon Sep 17 00:00:00 2001 From: Tietew Date: Thu, 30 Jan 2025 12:57:11 +0900 Subject: [PATCH 08/19] eslint fix --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index 720a8c819e7b1..f3c8386aa03da 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -489,7 +489,7 @@ export interface AuthFactor { * @default false */ readonly passkey?: boolean; -}; +} /** * The user-pool treatment for MFA with a passkey @@ -500,7 +500,7 @@ export enum PasskeyUserVerification { PREFERRED = 'preferred', /** Passkey MFA is required */ REQUIRED = 'required', -}; +} /** * Email settings for the user pool. @@ -1434,7 +1434,7 @@ export class UserPool extends UserPoolBase { } if (props.featurePlan === FeaturePlan.LITE) { - throw new Error('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); + throw new ValidationError('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this); } return { allowedFirstAuthFactors }; From 882cf415a03507757b37be694f9a0b746e9a1d37 Mon Sep 17 00:00:00 2001 From: Tietew Date: Mon, 3 Feb 2025 11:13:07 +0900 Subject: [PATCH 09/19] Revert "return undefined when no allowed factors" This reverts commit 6b65ce006cc02f42bd1831f2a6a90c1922dcccb4. --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 12 ++++-------- .../aws-cdk-lib/aws-cognito/test/user-pool.test.ts | 6 ++---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index d4416e0dec416..f964133c1bb7a 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -1421,6 +1421,10 @@ export class UserPool extends UserPoolBase { return undefined; } + if (props.featurePlan === FeaturePlan.LITE) { + throw new ValidationError('To enable passwordless sign-in, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this); + } + // PASSWORD should be configured as one of the allowed first auth factors. const allowedFirstAuthFactors = ['PASSWORD']; if (props.allowedFirstAuthFactors.emailOtp) { @@ -1432,14 +1436,6 @@ export class UserPool extends UserPoolBase { if (props.allowedFirstAuthFactors.passkey) { allowedFirstAuthFactors.push('WEB_AUTHN'); } - if (allowedFirstAuthFactors.length === 1) { - return undefined; - } - - if (props.featurePlan === FeaturePlan.LITE) { - throw new ValidationError('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this); - } - return { allowedFirstAuthFactors }; } diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index 919cfa8f2a390..1786e605e6362 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2091,9 +2091,7 @@ describe('User Pool', () => { const stack = new Stack(); // WHEN - new UserPool(stack, 'Pool', { - allowedFirstAuthFactors: {}, - }); + new UserPool(stack, 'Pool', {}); // THEN Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { @@ -2139,7 +2137,7 @@ describe('User Pool', () => { allowedFirstAuthFactors: { emailOtp: true }, featurePlan: FeaturePlan.LITE, }); - }).toThrow('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); + }).toThrow('To enable passwordless sign-in, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); }); test('passkeyRelyingPartyId is configured', () => { From 27eb2742083277d75f4dfaa89424f07878f675ff Mon Sep 17 00:00:00 2001 From: Tietew Date: Mon, 3 Feb 2025 11:49:19 +0900 Subject: [PATCH 10/19] test explicit blank --- .../aws-cognito/test/user-pool.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index 1786e605e6362..114b0f8eb85c2 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2099,6 +2099,25 @@ describe('User Pool', () => { }); }); + test('allowFirstAuthFactors contains only PASSWORD when option is blank', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + allowedFirstAuthFactors: {}, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + Policies: { + SignInPolicy: { + AllowedFirstAuthFactors: ['PASSWORD'], + }, + }, + }); + }); + test('allowFirstAuthFactors are correctly named', () => { // GIVEN const stack = new Stack(); From 69e8b8b274248bb6e1ef03654acaeb7788ca0041 Mon Sep 17 00:00:00 2001 From: Tietew Date: Mon, 3 Feb 2025 11:51:53 +0900 Subject: [PATCH 11/19] choice-based authentication --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 2 +- packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index f964133c1bb7a..320ae69a8978b 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -1422,7 +1422,7 @@ export class UserPool extends UserPoolBase { } if (props.featurePlan === FeaturePlan.LITE) { - throw new ValidationError('To enable passwordless sign-in, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this); + throw new ValidationError('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this); } // PASSWORD should be configured as one of the allowed first auth factors. diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index 114b0f8eb85c2..a13374df5cadb 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2156,7 +2156,7 @@ describe('User Pool', () => { allowedFirstAuthFactors: { emailOtp: true }, featurePlan: FeaturePlan.LITE, }); - }).toThrow('To enable passwordless sign-in, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); + }).toThrow('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); }); test('passkeyRelyingPartyId is configured', () => { From 72501dc34cee7ab802c6c1bd8dbb35ebcca700c9 Mon Sep 17 00:00:00 2001 From: Tietew Date: Tue, 4 Feb 2025 11:36:18 +0900 Subject: [PATCH 12/19] add explicit password auth factor --- packages/aws-cdk-lib/aws-cognito/README.md | 11 +++++- .../aws-cdk-lib/aws-cognito/lib/user-pool.ts | 29 +++++++++++--- .../aws-cognito/test/user-pool.test.ts | 39 ++++++++++++------- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/README.md b/packages/aws-cdk-lib/aws-cognito/README.md index b742dd1c37791..f6fe3eebbe70e 100644 --- a/packages/aws-cdk-lib/aws-cognito/README.md +++ b/packages/aws-cdk-lib/aws-cognito/README.md @@ -231,7 +231,7 @@ const userPool = new cognito.UserPool(this, 'myuserpool', { }, }); -// You should also configure the user pool client to allow USER_AUTH authentication flow +// You should also configure the user pool client with USER_AUTH authentication flow allowed userPool.addClient('myclient', { authFlows: { user: true }, }); @@ -267,6 +267,15 @@ new cognito.UserPool(this, 'myuserpool', { }); ``` +To disable choice-based authentication explicitly, specify `{ password: true }`. + +```ts +new cognito.UserPool(this, 'myuserpool', { + allowedFirstAuthFactors: { password: true }, + featurePlan: cognito.FeaturePlan.LITE, +}); +``` + ### Attributes Attributes represent the various properties of each user that's collected and stored in the user pool. Cognito diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index 320ae69a8978b..6f1f6c85d882d 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -476,17 +476,23 @@ export interface PasswordPolicy { */ export interface AuthFactor { /** - * Whether the email message one-time password is allowed + * Whether the password authentication is allowed. + * This must be true when specified. + * @default 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 + * Whether the SMS message one-time password is allowed. * @default false */ readonly smsOtp?: boolean; /** - * Whether the Passkey (WebAuthn) is allowed + * Whether the Passkey (WebAuthn) is allowed. * @default false */ readonly passkey?: boolean; @@ -1421,11 +1427,11 @@ export class UserPool extends UserPoolBase { return undefined; } - if (props.featurePlan === FeaturePlan.LITE) { - throw new ValidationError('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this); + // PASSWORD should be configured as one of the allowed first auth factors. + if (props.allowedFirstAuthFactors.password === false) { + throw new ValidationError('The password authentication cannot be disabled.', this); } - // PASSWORD should be configured as one of the allowed first auth factors. const allowedFirstAuthFactors = ['PASSWORD']; if (props.allowedFirstAuthFactors.emailOtp) { allowedFirstAuthFactors.push('EMAIL_OTP'); @@ -1436,6 +1442,17 @@ export class UserPool extends UserPoolBase { if (props.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 isChouseBasedAuthenticationEnabled = allowedFirstAuthFactors.some((auth) => auth !== 'PASSWORD'); + if (isChouseBasedAuthenticationEnabled && props.featurePlan === FeaturePlan.LITE) { + throw new ValidationError('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this); + } + return { allowedFirstAuthFactors }; } diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index a13374df5cadb..813c566055d9e 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2099,23 +2099,16 @@ describe('User Pool', () => { }); }); - test('allowFirstAuthFactors contains only PASSWORD when option is blank', () => { + test('allowFirstAuthFactors throws when password is false', () => { // GIVEN const stack = new Stack(); // WHEN - new UserPool(stack, 'Pool', { - allowedFirstAuthFactors: {}, - }); - - // THEN - Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { - Policies: { - SignInPolicy: { - AllowedFirstAuthFactors: ['PASSWORD'], - }, - }, - }); + expect(() => { + new UserPool(stack, 'Pool', { + allowedFirstAuthFactors: { password: false }, + }); + }).toThrow('The password authentication cannot be disabled.'); }); test('allowFirstAuthFactors are correctly named', () => { @@ -2159,6 +2152,26 @@ describe('User Pool', () => { }).toThrow('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); }); + test('allowFirstAuthFactors can specify only password when the feature plan is Lite', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + allowedFirstAuthFactors: { password: true }, + featurePlan: FeaturePlan.LITE, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { + Policies: { + SignInPolicy: { + AllowedFirstAuthFactors: ['PASSWORD'], + }, + }, + }); + }); + test('passkeyRelyingPartyId is configured', () => { // GIVEN const stack = new Stack(); From 92e1e1100f6d15dfd66d6d8aa435c95a1692e56d Mon Sep 17 00:00:00 2001 From: Tietew Date: Tue, 4 Feb 2025 12:20:09 +0900 Subject: [PATCH 13/19] add iteg test explicit disable choice-based authentication --- ...efaultTestDeployAssertE3E7D2A4.assets.json | 2 +- .../cdk.out | 2 +- .../integ-user-pool-passwordless.assets.json | 6 +- ...integ-user-pool-passwordless.template.json | 40 +++++++++++++ .../integ.json | 2 +- .../manifest.json | 10 +++- .../tree.json | 60 ++++++++++++++++++- .../test/integ.user-pool-passwordless.ts | 9 ++- 8 files changed, 121 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json index 8bbe077289ad2..7fc0c7c6c51ec 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json @@ -1,5 +1,5 @@ { - "version": "38.0.1", + "version": "39.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/cdk.out index c6e612584e352..91e1a8b9901d5 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"38.0.1"} \ No newline at end of file +{"version":"39.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.assets.json index 557ed52dc5dbc..f4d4760b986ac 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.assets.json @@ -1,7 +1,7 @@ { - "version": "38.0.1", + "version": "39.0.0", "files": { - "e8fd9461c2a642a58e740da0d66211db2c1e638fb438602d9cd589f69d8df18e": { + "7253c171dd493c91d49e70e9d3f5326c0817f3fb036cbef8a3b43a746e1833c3": { "source": { "path": "integ-user-pool-passwordless.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "e8fd9461c2a642a58e740da0d66211db2c1e638fb438602d9cd589f69d8df18e.json", + "objectKey": "7253c171dd493c91d49e70e9d3f5326c0817f3fb036cbef8a3b43a746e1833c3.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.template.json index ef5fecb5b81b0..392e3317840ab 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ-user-pool-passwordless.template.json @@ -42,6 +42,46 @@ }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" + }, + "myuserpooldisabledpasswordless25B4C801": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "DeletionProtection": "INACTIVE", + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "Policies": { + "SignInPolicy": { + "AllowedFirstAuthFactors": [ + "PASSWORD" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolTier": "LITE", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" } }, "Outputs": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ.json index 1b47987489bd1..15e35240152d9 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "38.0.1", + "version": "39.0.0", "testCases": { "IntegTest/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/manifest.json index 3df9c0b283e4b..cf9839b869459 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "38.0.1", + "version": "39.0.0", "artifacts": { "integ-user-pool-passwordless.assets": { "type": "cdk:asset-manifest", @@ -18,7 +18,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/e8fd9461c2a642a58e740da0d66211db2c1e638fb438602d9cd589f69d8df18e.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/7253c171dd493c91d49e70e9d3f5326c0817f3fb036cbef8a3b43a746e1833c3.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -40,6 +40,12 @@ "data": "myuserpool01998219" } ], + "/integ-user-pool-passwordless/myuserpool-disabled-passwordless/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "myuserpooldisabledpasswordless25B4C801" + } + ], "/integ-user-pool-passwordless/user-pool-passwordless": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/tree.json index 6a8f3dba7d17a..5ba032a4ff765 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.js.snapshot/tree.json @@ -64,7 +64,65 @@ }, "constructInfo": { "fqn": "aws-cdk-lib.aws_cognito.UserPool", - "version": "0.0.0" + "version": "0.0.0", + "metadata": [] + } + }, + "myuserpool-disabled-passwordless": { + "id": "myuserpool-disabled-passwordless", + "path": "integ-user-pool-passwordless/myuserpool-disabled-passwordless", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-user-pool-passwordless/myuserpool-disabled-passwordless/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Cognito::UserPool", + "aws:cdk:cloudformation:props": { + "accountRecoverySetting": { + "recoveryMechanisms": [ + { + "name": "verified_phone_number", + "priority": 1 + }, + { + "name": "verified_email", + "priority": 2 + } + ] + }, + "adminCreateUserConfig": { + "allowAdminCreateUserOnly": true + }, + "deletionProtection": "INACTIVE", + "emailVerificationMessage": "The verification code to your new account is {####}", + "emailVerificationSubject": "Verify your new account", + "policies": { + "signInPolicy": { + "allowedFirstAuthFactors": [ + "PASSWORD" + ] + } + }, + "smsVerificationMessage": "The verification code to your new account is {####}", + "userPoolTier": "LITE", + "verificationMessageTemplate": { + "defaultEmailOption": "CONFIRM_WITH_CODE", + "emailMessage": "The verification code to your new account is {####}", + "emailSubject": "Verify your new account", + "smsMessage": "The verification code to your new account is {####}" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cognito.CfnUserPool", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cognito.UserPool", + "version": "0.0.0", + "metadata": [] } }, "user-pool-passwordless": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts index 5abdc5839ab0a..eb5f04cc4a721 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts @@ -1,6 +1,6 @@ import { IntegTest } from '@aws-cdk/integ-tests-alpha'; import { App, CfnOutput, RemovalPolicy, Stack } from 'aws-cdk-lib'; -import { PasskeyUserVerification, UserPool } from 'aws-cdk-lib/aws-cognito'; +import { FeaturePlan, PasskeyUserVerification, UserPool } from 'aws-cdk-lib/aws-cognito'; const app = new App(); const stack = new Stack(app, 'integ-user-pool-passwordless'); @@ -13,6 +13,13 @@ const userpool = new UserPool(stack, 'myuserpool', { deletionProtection: false, }); +new UserPool(stack, 'myuserpool-disabled-passwordless', { + allowedFirstAuthFactors: { password: true }, + featurePlan: FeaturePlan.LITE, + removalPolicy: RemovalPolicy.DESTROY, + deletionProtection: false, +}); + new CfnOutput(stack, 'user-pool-passwordless', { value: userpool.userPoolId, }); From b1a07b1e60e8bd6c3bc5b852ec73759bc7d72900 Mon Sep 17 00:00:00 2001 From: Tietew Date: Tue, 4 Feb 2025 19:06:41 +0900 Subject: [PATCH 14/19] require `password: true` always --- .../test/integ.user-pool-passwordless.ts | 2 +- packages/aws-cdk-lib/aws-cognito/README.md | 9 +++++---- .../aws-cdk-lib/aws-cognito/lib/user-pool.ts | 16 +++++++++------- .../aws-cognito/test/user-pool.test.ts | 3 ++- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts index eb5f04cc4a721..cddada78eb71f 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts @@ -6,7 +6,7 @@ const app = new App(); const stack = new Stack(app, 'integ-user-pool-passwordless'); const userpool = new UserPool(stack, 'myuserpool', { - allowedFirstAuthFactors: { emailOtp: true, passkey: true }, + allowedFirstAuthFactors: { password: true, emailOtp: true, passkey: true }, passkeyRelyingPartyId: 'example.com', passkeyUserVerification: PasskeyUserVerification.REQUIRED, removalPolicy: RemovalPolicy.DESTROY, diff --git a/packages/aws-cdk-lib/aws-cognito/README.md b/packages/aws-cdk-lib/aws-cognito/README.md index f6fe3eebbe70e..5234df3d0cbc5 100644 --- a/packages/aws-cdk-lib/aws-cognito/README.md +++ b/packages/aws-cdk-lib/aws-cognito/README.md @@ -225,6 +225,7 @@ The following code configures a user pool with choice-based authentication enabl ```ts const userPool = new cognito.UserPool(this, 'myuserpool', { allowedFirstAuthFactors: { + password: true, // password authentication must be enabled emailOtp: true, // enables email message one-time password smsOtp: true, // enables SMS message one-time password passkey: true, // enables passkey sign-in @@ -246,13 +247,13 @@ Learn more about [passkey sign-in of user pools](https://docs.aws.amazon.com/cog ```ts // Use the hosted Amazon Cognito domain as the relying party ID new cognito.UserPool(this, 'myuserpool', { - allowedFirstAuthFactors: { passkey: true }, + allowedFirstAuthFactors: { password: true, passkey: true }, passkeyRelyingPartyId: 'myclientname.auth.region-name.amazoncognito.com', }); // Use the custom domain as the relying party ID new cognito.UserPool(this, 'myuserpool', { - allowedFirstAuthFactors: { passkey: true }, + allowedFirstAuthFactors: { password: true, passkey: true }, passkeyRelyingPartyId: 'auth.example.com', }); ``` @@ -261,13 +262,13 @@ You can configure user verification to be preferred (default) or required. When ```ts new cognito.UserPool(this, 'myuserpool', { - allowedFirstAuthFactors: { passkey: true }, + allowedFirstAuthFactors: { password: true, passkey: true }, passkeyRelyingPartyId: 'auth.example.com', passkeyUserVerification: cognito.PasskeyUserVerification.REQUIRED, }); ``` -To disable choice-based authentication explicitly, specify `{ password: true }`. +To disable choice-based authentication explicitly, specify `password` only. ```ts new cognito.UserPool(this, 'myuserpool', { diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index 6f1f6c85d882d..e7905f7bd6608 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -477,10 +477,9 @@ export interface PasswordPolicy { export interface AuthFactor { /** * Whether the password authentication is allowed. - * This must be true when specified. - * @default true + * This must be true. */ - readonly password?: boolean; + readonly password: boolean; /** * Whether the email message one-time password is allowed. * @default false @@ -1428,11 +1427,14 @@ export class UserPool extends UserPoolBase { } // PASSWORD should be configured as one of the allowed first auth factors. - if (props.allowedFirstAuthFactors.password === false) { + if (!props.allowedFirstAuthFactors.password) { throw new ValidationError('The password authentication cannot be disabled.', this); } - const allowedFirstAuthFactors = ['PASSWORD']; + const allowedFirstAuthFactors = []; + if (props.allowedFirstAuthFactors.password) { + allowedFirstAuthFactors.push('PASSWORD'); + } if (props.allowedFirstAuthFactors.emailOtp) { allowedFirstAuthFactors.push('EMAIL_OTP'); } @@ -1448,8 +1450,8 @@ export class UserPool extends UserPoolBase { * This check should be placed here to supply the way to disable choice-based authentication explicitly * by specifying `allowedFirstAuthFactors: { password: true }`. */ - const isChouseBasedAuthenticationEnabled = allowedFirstAuthFactors.some((auth) => auth !== 'PASSWORD'); - if (isChouseBasedAuthenticationEnabled && props.featurePlan === FeaturePlan.LITE) { + const isChoiseBasedAuthenticationEnabled = allowedFirstAuthFactors.some((auth) => auth !== 'PASSWORD'); + if (isChoiseBasedAuthenticationEnabled && props.featurePlan === FeaturePlan.LITE) { throw new ValidationError('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.', this); } diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index 813c566055d9e..61572170735ec 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2118,6 +2118,7 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { allowedFirstAuthFactors: { + password: true, emailOtp: true, smsOtp: true, passkey: true, @@ -2146,7 +2147,7 @@ describe('User Pool', () => { // WHEN expect(() => { new UserPool(stack, 'Pool', { - allowedFirstAuthFactors: { emailOtp: true }, + allowedFirstAuthFactors: { password: true, emailOtp: true }, featurePlan: FeaturePlan.LITE, }); }).toThrow('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); From 9cf28149df0437b6179330ac5b4419fef71e5b81 Mon Sep 17 00:00:00 2001 From: Tietew Date: Tue, 4 Feb 2025 20:05:58 +0900 Subject: [PATCH 15/19] fix typo --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index e7905f7bd6608..a3f32d7bb4333 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -1450,8 +1450,8 @@ export class UserPool extends UserPoolBase { * This check should be placed here to supply the way to disable choice-based authentication explicitly * by specifying `allowedFirstAuthFactors: { password: true }`. */ - const isChoiseBasedAuthenticationEnabled = allowedFirstAuthFactors.some((auth) => auth !== 'PASSWORD'); - if (isChoiseBasedAuthenticationEnabled && props.featurePlan === FeaturePlan.LITE) { + 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); } From 79e6c9a75a0c994c03b1202468d67572c7bd1b4a Mon Sep 17 00:00:00 2001 From: Tietew Date: Thu, 13 Feb 2025 11:32:31 +0900 Subject: [PATCH 16/19] Update packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts Co-authored-by: Samson Keung --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index a3f32d7bb4333..1e3635cc5e7fa 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -1426,7 +1426,7 @@ export class UserPool extends UserPoolBase { return undefined; } - // PASSWORD should be configured as one of the allowed first auth factors. + // As of writing, from testing, CFN deployment will fail if `PASSWORD` is not enabled. if (!props.allowedFirstAuthFactors.password) { throw new ValidationError('The password authentication cannot be disabled.', this); } From 9df99cebcd80b6ec29a8a65e6cc365cc0a61cadc Mon Sep 17 00:00:00 2001 From: Tietew Date: Thu, 13 Feb 2025 12:05:28 +0900 Subject: [PATCH 17/19] allowedFirstAuthFactors wrapped in signInPolicy --- .../test/integ.user-pool-passwordless.ts | 8 ++- packages/aws-cdk-lib/aws-cognito/README.md | 28 +++++--- .../aws-cdk-lib/aws-cognito/lib/user-pool.ts | 68 +++++++++++-------- .../aws-cognito/test/user-pool.test.ts | 30 +++++--- 4 files changed, 84 insertions(+), 50 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts index cddada78eb71f..581a9ed0e2527 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cognito/test/integ.user-pool-passwordless.ts @@ -6,7 +6,9 @@ const app = new App(); const stack = new Stack(app, 'integ-user-pool-passwordless'); const userpool = new UserPool(stack, 'myuserpool', { - allowedFirstAuthFactors: { password: true, emailOtp: true, passkey: true }, + signInPolicy: { + allowedFirstAuthFactors: { password: true, emailOtp: true, passkey: true }, + }, passkeyRelyingPartyId: 'example.com', passkeyUserVerification: PasskeyUserVerification.REQUIRED, removalPolicy: RemovalPolicy.DESTROY, @@ -14,7 +16,9 @@ const userpool = new UserPool(stack, 'myuserpool', { }); new UserPool(stack, 'myuserpool-disabled-passwordless', { - allowedFirstAuthFactors: { password: true }, + signInPolicy: { + allowedFirstAuthFactors: { password: true }, + }, featurePlan: FeaturePlan.LITE, removalPolicy: RemovalPolicy.DESTROY, deletionProtection: false, diff --git a/packages/aws-cdk-lib/aws-cognito/README.md b/packages/aws-cdk-lib/aws-cognito/README.md index f4dffcac3bafe..f63ed127718dd 100644 --- a/packages/aws-cdk-lib/aws-cognito/README.md +++ b/packages/aws-cdk-lib/aws-cognito/README.md @@ -227,11 +227,13 @@ The following code configures a user pool with choice-based authentication enabl ```ts const userPool = new cognito.UserPool(this, 'myuserpool', { - allowedFirstAuthFactors: { - password: true, // password authentication must be enabled - emailOtp: true, // enables email message one-time password - smsOtp: true, // enables SMS message one-time password - passkey: true, // enables passkey sign-in + signInPolicy: { + allowedFirstAuthFactors: { + password: true, // password authentication must be enabled + emailOtp: true, // enables email message one-time password + smsOtp: true, // enables SMS message one-time password + passkey: true, // enables passkey sign-in + }, }, }); @@ -250,13 +252,17 @@ Learn more about [passkey sign-in of user pools](https://docs.aws.amazon.com/cog ```ts // Use the hosted Amazon Cognito domain as the relying party ID new cognito.UserPool(this, 'myuserpool', { - allowedFirstAuthFactors: { password: true, passkey: true }, + signInPolicy: { + allowedFirstAuthFactors: { password: true, passkey: true }, + }, passkeyRelyingPartyId: 'myclientname.auth.region-name.amazoncognito.com', }); // Use the custom domain as the relying party ID new cognito.UserPool(this, 'myuserpool', { - allowedFirstAuthFactors: { password: true, passkey: true }, + signInPolicy: { + allowedFirstAuthFactors: { password: true, passkey: true }, + }, passkeyRelyingPartyId: 'auth.example.com', }); ``` @@ -265,7 +271,9 @@ You can configure user verification to be preferred (default) or required. When ```ts new cognito.UserPool(this, 'myuserpool', { - allowedFirstAuthFactors: { password: true, passkey: true }, + signInPolicy: { + allowedFirstAuthFactors: { password: true, passkey: true }, + }, passkeyRelyingPartyId: 'auth.example.com', passkeyUserVerification: cognito.PasskeyUserVerification.REQUIRED, }); @@ -275,7 +283,9 @@ To disable choice-based authentication explicitly, specify `password` only. ```ts new cognito.UserPool(this, 'myuserpool', { - allowedFirstAuthFactors: { password: true }, + signInPolicy: { + allowedFirstAuthFactors: { password: true }, + }, featurePlan: cognito.FeaturePlan.LITE, }); ``` diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index 0cedc48090af7..a965261358faa 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -470,11 +470,25 @@ export interface PasswordPolicy { 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. + * The password authentication is allowed always. + * @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 AuthFactor { +export interface AllowedFirstAuthFactors { /** * Whether the password authentication is allowed. * This must be true. @@ -758,13 +772,10 @@ export interface UserPoolProps { readonly passwordPolicy?: PasswordPolicy; /** - * The types of authentication that you want to allow for users' first authentication prompt. - * The password authentication is allowed always. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-selection-sdk.html#authentication-flows-selection-choice - * - * @default - Password only + * Sign-in policy for this user pool. + * @default - see defaults on each property of SignInPolicy. */ - readonly allowedFirstAuthFactors?: AuthFactor; + readonly signInPolicy?: SignInPolicy; /** * The authentication domain that passkey providers must use as a relying party (RP) in their configuration. @@ -1423,27 +1434,26 @@ export class UserPool extends UserPoolBase { } private configureSignInPolicy(props: UserPoolProps): CfnUserPool.SignInPolicyProperty | undefined { - if (!props.allowedFirstAuthFactors) { - return undefined; - } - - // As of writing, from testing, CFN deployment will fail if `PASSWORD` is not enabled. - if (!props.allowedFirstAuthFactors.password) { - throw new ValidationError('The password authentication cannot be disabled.', this); - } + 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); + } - const allowedFirstAuthFactors = []; - if (props.allowedFirstAuthFactors.password) { - allowedFirstAuthFactors.push('PASSWORD'); - } - if (props.allowedFirstAuthFactors.emailOtp) { - allowedFirstAuthFactors.push('EMAIL_OTP'); - } - if (props.allowedFirstAuthFactors.smsOtp) { - allowedFirstAuthFactors.push('SMS_OTP'); - } - if (props.allowedFirstAuthFactors.passkey) { - allowedFirstAuthFactors.push('WEB_AUTHN'); + 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'); + } } /* @@ -1451,12 +1461,12 @@ export class UserPool extends UserPoolBase { * 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'); + 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 { allowedFirstAuthFactors }; + return undefinedIfNoKeys({ allowedFirstAuthFactors }); } private schemaConfiguration(props: UserPoolProps): CfnUserPool.SchemaAttributeProperty[] | undefined { diff --git a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts index 61572170735ec..47e09e4c1c49a 100644 --- a/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts +++ b/packages/aws-cdk-lib/aws-cognito/test/user-pool.test.ts @@ -2086,12 +2086,14 @@ describe('User Pool', () => { })).toThrow(/"fromEmail" contains a different domain than the "sesVerifiedDomain"/); }); - test('allowFirstAuthFactors is not present if option is not provided', () => { + test('signInPolicy is not present if none of options are not provided', () => { // GIVEN const stack = new Stack(); // WHEN - new UserPool(stack, 'Pool', {}); + new UserPool(stack, 'Pool', { + signInPolicy: {}, + }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::Cognito::UserPool', { @@ -2106,7 +2108,9 @@ describe('User Pool', () => { // WHEN expect(() => { new UserPool(stack, 'Pool', { - allowedFirstAuthFactors: { password: false }, + signInPolicy: { + allowedFirstAuthFactors: { password: false }, + }, }); }).toThrow('The password authentication cannot be disabled.'); }); @@ -2117,11 +2121,13 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - allowedFirstAuthFactors: { - password: true, - emailOtp: true, - smsOtp: true, - passkey: true, + signInPolicy: { + allowedFirstAuthFactors: { + password: true, + emailOtp: true, + smsOtp: true, + passkey: true, + }, }, }); @@ -2147,7 +2153,9 @@ describe('User Pool', () => { // WHEN expect(() => { new UserPool(stack, 'Pool', { - allowedFirstAuthFactors: { password: true, emailOtp: true }, + signInPolicy: { + allowedFirstAuthFactors: { password: true, emailOtp: true }, + }, featurePlan: FeaturePlan.LITE, }); }).toThrow('To enable choice-based authentication, set `featurePlan` to `FeaturePlan.ESSENTIALS` or `FeaturePlan.PLUS`.'); @@ -2159,7 +2167,9 @@ describe('User Pool', () => { // WHEN new UserPool(stack, 'Pool', { - allowedFirstAuthFactors: { password: true }, + signInPolicy: { + allowedFirstAuthFactors: { password: true }, + }, featurePlan: FeaturePlan.LITE, }); From c2d1346e2e3a2cb87a5849eeefd787596fa3d7c4 Mon Sep 17 00:00:00 2001 From: Tietew Date: Thu, 13 Feb 2025 12:10:53 +0900 Subject: [PATCH 18/19] update doc --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index a965261358faa..1d23937eebfcc 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -794,7 +794,7 @@ export interface UserPoolProps { * 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 PasskeyUserVerification.PREFERRED + * @default - Cognito default setting is PasskeyUserVerification.PREFERRED */ readonly passkeyUserVerification?: PasskeyUserVerification; From 31b26cc1f31cbb46f081020cb6927a3df1b1d3e2 Mon Sep 17 00:00:00 2001 From: Tietew Date: Thu, 13 Feb 2025 12:17:43 +0900 Subject: [PATCH 19/19] password is now mandatory to specify true --- packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts index 1d23937eebfcc..14ea16adb5921 100644 --- a/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts +++ b/packages/aws-cdk-lib/aws-cognito/lib/user-pool.ts @@ -476,7 +476,6 @@ export interface PasswordPolicy { export interface SignInPolicy { /** * The types of authentication that you want to allow for users' first authentication prompt. - * The password authentication is allowed always. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-selection-sdk.html#authentication-flows-selection-choice * * @default - Password only