diff --git a/infra-gen2/backends/auth/mfa-optional-email-sms/.gitignore b/infra-gen2/backends/auth/mfa-optional-email-sms/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-sms/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/auth/resource.ts b/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/auth/resource.ts new file mode 100644 index 0000000000..43ddc18eec --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/auth/resource.ts @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineAuth } from "@aws-amplify/backend"; + +export const auth = defineAuth({ + name: "mfa-optional-email-sms", + loginWith: { + email: true, + }, + + // TODO(khatruong2009): Uncomment the following line when the feature is ready. + // multifactor: { + // mode: "OPTIONAL", + // email: true, + // sms: true, + // }, +}); diff --git a/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/backend.ts b/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/backend.ts new file mode 100644 index 0000000000..d6b854225f --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/backend.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineBackend } from "@aws-amplify/backend"; +import { addAuthUserExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +const backend = defineBackend({ + auth, +}); + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; +const { cfnUserPool } = cfnResources; + +// Adds infra for creating/deleting users via App Sync and fetching confirmation +// and MFA codes from App Sync. +const customOutputs = addAuthUserExtensions({ + name: "mfa-optional-email-sms", + stack, + userPool, + cfnUserPool, +}); +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/package.json b/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/tsconfig.json b/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-sms/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-optional-email-sms/package.json b/infra-gen2/backends/auth/mfa-optional-email-sms/package.json new file mode 100644 index 0000000000..f8ffc5b939 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-sms/package.json @@ -0,0 +1,5 @@ +{ + "name": "mfa-optional-email-sms", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/backends/auth/mfa-optional-email-totp/.gitignore b/infra-gen2/backends/auth/mfa-optional-email-totp/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-totp/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/auth/resource.ts b/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/auth/resource.ts new file mode 100644 index 0000000000..ae1765e5d0 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/auth/resource.ts @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineAuth } from "@aws-amplify/backend"; + +export const auth = defineAuth({ + name: "mfa-optional-email-totp", + loginWith: { + email: true, + }, + + // TODO(khatruong2009): Uncomment the following line when the feature is ready. + // multifactor: { + // mode: "OPTIONAL", + // email: true, + // totp: true, + // }, +}); diff --git a/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/backend.ts b/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/backend.ts new file mode 100644 index 0000000000..a3f1f19985 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/backend.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineBackend } from "@aws-amplify/backend"; +import { addAuthUserExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +const backend = defineBackend({ + auth, +}); + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; +const { cfnUserPool } = cfnResources; + +// Adds infra for creating/deleting users via App Sync and fetching confirmation +// and MFA codes from App Sync. +const customOutputs = addAuthUserExtensions({ + name: "mfa-optional-email-totp", + stack, + userPool, + cfnUserPool, +}); +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/package.json b/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/tsconfig.json b/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-totp/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-optional-email-totp/package.json b/infra-gen2/backends/auth/mfa-optional-email-totp/package.json new file mode 100644 index 0000000000..20be47dae0 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email-totp/package.json @@ -0,0 +1,5 @@ +{ + "name": "mfa-optional-email-totp", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/backends/auth/mfa-optional-email/.gitignore b/infra-gen2/backends/auth/mfa-optional-email/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/auth/mfa-optional-email/amplify/auth/resource.ts b/infra-gen2/backends/auth/mfa-optional-email/amplify/auth/resource.ts new file mode 100644 index 0000000000..1e33905c1d --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email/amplify/auth/resource.ts @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineAuth } from "@aws-amplify/backend"; + +export const auth = defineAuth({ + name: "mfa-optional-email", + loginWith: { + email: true, + }, + + // TODO(khatruong2009): Uncomment the following line when the feature is ready. + // multifactor: { + // mode: "OPTIONAL", + // email: true, + // }, +}); diff --git a/infra-gen2/backends/auth/mfa-optional-email/amplify/backend.ts b/infra-gen2/backends/auth/mfa-optional-email/amplify/backend.ts new file mode 100644 index 0000000000..ceb5e2a90a --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email/amplify/backend.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineBackend } from "@aws-amplify/backend"; +import { addAuthUserExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +const backend = defineBackend({ + auth, +}); + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; +const { cfnUserPool } = cfnResources; + +// Adds infra for creating/deleting users via App Sync and fetching confirmation +// and MFA codes from App Sync. +const customOutputs = addAuthUserExtensions({ + name: "mfa-optional-email", + stack, + userPool, + cfnUserPool, +}); +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/auth/mfa-optional-email/amplify/package.json b/infra-gen2/backends/auth/mfa-optional-email/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-optional-email/amplify/tsconfig.json b/infra-gen2/backends/auth/mfa-optional-email/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-optional-email/package.json b/infra-gen2/backends/auth/mfa-optional-email/package.json new file mode 100644 index 0000000000..a716d71dd4 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-optional-email/package.json @@ -0,0 +1,5 @@ +{ + "name": "mfa-optional-email", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/backends/auth/mfa-required-email-sms/.gitignore b/infra-gen2/backends/auth/mfa-required-email-sms/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-sms/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/auth/mfa-required-email-sms/amplify/auth/resource.ts b/infra-gen2/backends/auth/mfa-required-email-sms/amplify/auth/resource.ts new file mode 100644 index 0000000000..e90924c42e --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-sms/amplify/auth/resource.ts @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineAuth } from "@aws-amplify/backend"; + +export const auth = defineAuth({ + name: "mfa-required-email-sms", + loginWith: { + email: true, + }, + // TODO(khatruong2009): Uncomment the following line when the feature is ready. + // multifactor: { + // mode: "REQUIRED", + // email: true, + // sms: true, + // }, +}); diff --git a/infra-gen2/backends/auth/mfa-required-email-sms/amplify/backend.ts b/infra-gen2/backends/auth/mfa-required-email-sms/amplify/backend.ts new file mode 100644 index 0000000000..397b7b4a6e --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-sms/amplify/backend.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineBackend } from "@aws-amplify/backend"; +import { addAuthUserExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +const backend = defineBackend({ + auth, +}); + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; +const { cfnUserPool } = cfnResources; + +// Adds infra for creating/deleting users via App Sync and fetching confirmation +// and MFA codes from App Sync. +const customOutputs = addAuthUserExtensions({ + name: "mfa-required-email-sms", + stack, + userPool, + cfnUserPool, +}); +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/auth/mfa-required-email-sms/amplify/package.json b/infra-gen2/backends/auth/mfa-required-email-sms/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-sms/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-required-email-sms/amplify/tsconfig.json b/infra-gen2/backends/auth/mfa-required-email-sms/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-sms/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-required-email-sms/package.json b/infra-gen2/backends/auth/mfa-required-email-sms/package.json new file mode 100644 index 0000000000..b2e8c9c4c7 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-sms/package.json @@ -0,0 +1,5 @@ +{ + "name": "mfa-required-email-sms", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/backends/auth/mfa-required-email-totp/.gitignore b/infra-gen2/backends/auth/mfa-required-email-totp/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-totp/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/auth/mfa-required-email-totp/amplify/auth/resource.ts b/infra-gen2/backends/auth/mfa-required-email-totp/amplify/auth/resource.ts new file mode 100644 index 0000000000..bf09fd7dc7 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-totp/amplify/auth/resource.ts @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineAuth } from "@aws-amplify/backend"; + +export const auth = defineAuth({ + name: "mfa-required-email-totp", + loginWith: { + email: true, + }, + // TODO(khatruong2009): Uncomment the following line when the feature is ready. + // multifactor: { + // mode: "REQUIRED", + // email: true, + // totp: true, + // }, +}); diff --git a/infra-gen2/backends/auth/mfa-required-email-totp/amplify/backend.ts b/infra-gen2/backends/auth/mfa-required-email-totp/amplify/backend.ts new file mode 100644 index 0000000000..c9e3bd79c1 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-totp/amplify/backend.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineBackend } from "@aws-amplify/backend"; +import { addAuthUserExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +const backend = defineBackend({ + auth, +}); + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; +const { cfnUserPool } = cfnResources; + +// Adds infra for creating/deleting users via App Sync and fetching confirmation +// and MFA codes from App Sync. +const customOutputs = addAuthUserExtensions({ + name: "mfa-required-email-totp", + stack, + userPool, + cfnUserPool, +}); +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/auth/mfa-required-email-totp/amplify/package.json b/infra-gen2/backends/auth/mfa-required-email-totp/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-totp/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-required-email-totp/amplify/tsconfig.json b/infra-gen2/backends/auth/mfa-required-email-totp/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-totp/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-required-email-totp/package.json b/infra-gen2/backends/auth/mfa-required-email-totp/package.json new file mode 100644 index 0000000000..1ebbbbb859 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email-totp/package.json @@ -0,0 +1,5 @@ +{ + "name": "mfa-required-email-totp", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/backends/auth/mfa-required-email/.gitignore b/infra-gen2/backends/auth/mfa-required-email/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/auth/mfa-required-email/amplify/auth/resource.ts b/infra-gen2/backends/auth/mfa-required-email/amplify/auth/resource.ts new file mode 100644 index 0000000000..ba859af386 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email/amplify/auth/resource.ts @@ -0,0 +1,16 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineAuth } from "@aws-amplify/backend"; + +export const auth = defineAuth({ + name: "mfa-required-email", + loginWith: { + email: true, + }, + // TODO(khatruong2009): Uncomment the following line when the feature is ready. + // multifactor: { + // mode: "REQUIRED", + // email: true, + // }, +}); diff --git a/infra-gen2/backends/auth/mfa-required-email/amplify/backend.ts b/infra-gen2/backends/auth/mfa-required-email/amplify/backend.ts new file mode 100644 index 0000000000..e9299cf711 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email/amplify/backend.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineBackend } from "@aws-amplify/backend"; +import { addAuthUserExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +const backend = defineBackend({ + auth, +}); + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; +const { cfnUserPool } = cfnResources; + +// Adds infra for creating/deleting users via App Sync and fetching confirmation +// and MFA codes from App Sync. +const customOutputs = addAuthUserExtensions({ + name: "mfa-required-email", + stack, + userPool, + cfnUserPool, +}); +backend.addOutput(customOutputs); diff --git a/infra-gen2/backends/auth/mfa-required-email/amplify/package.json b/infra-gen2/backends/auth/mfa-required-email/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-required-email/amplify/tsconfig.json b/infra-gen2/backends/auth/mfa-required-email/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/mfa-required-email/package.json b/infra-gen2/backends/auth/mfa-required-email/package.json new file mode 100644 index 0000000000..3d30b5522b --- /dev/null +++ b/infra-gen2/backends/auth/mfa-required-email/package.json @@ -0,0 +1,5 @@ +{ + "name": "mfa-required-email", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/backends/auth/username-login-mfa/.gitignore b/infra-gen2/backends/auth/username-login-mfa/.gitignore new file mode 100644 index 0000000000..03d4668c65 --- /dev/null +++ b/infra-gen2/backends/auth/username-login-mfa/.gitignore @@ -0,0 +1,5 @@ +# amplify +node_modules +.amplify +amplify_outputs* +amplifyconfiguration* diff --git a/infra-gen2/backends/auth/username-login-mfa/amplify/auth/resource.ts b/infra-gen2/backends/auth/username-login-mfa/amplify/auth/resource.ts new file mode 100644 index 0000000000..94d78b3d4b --- /dev/null +++ b/infra-gen2/backends/auth/username-login-mfa/amplify/auth/resource.ts @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineAuth } from "@aws-amplify/backend"; + +export const auth = defineAuth({ + name: "mfa-username-login", + loginWith: { + phone: true, + }, + // TODO(khatruong2009): Uncomment the following line when the feature is ready. + // multifactor: { + // mode: "REQUIRED", + // email: true, + // sms: true, + // totp: true, + // }, + accountRecovery: "PHONE_WITHOUT_MFA_AND_EMAIL", +}); diff --git a/infra-gen2/backends/auth/username-login-mfa/amplify/backend.ts b/infra-gen2/backends/auth/username-login-mfa/amplify/backend.ts new file mode 100644 index 0000000000..ce86475882 --- /dev/null +++ b/infra-gen2/backends/auth/username-login-mfa/amplify/backend.ts @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineBackend } from "@aws-amplify/backend"; +import { addAuthUserExtensions } from "infra-common"; +import { auth } from "./auth/resource"; + +const backend = defineBackend({ + auth, +}); + +const resources = backend.auth.resources; +const { userPool, cfnResources } = resources; +const { stack } = userPool; +const { cfnUserPool } = cfnResources; + +// Adds infra for creating/deleting users via App Sync and fetching confirmation +// and MFA codes from App Sync. +const customOutputs = addAuthUserExtensions({ + name: "username-login-mfa", + stack, + userPool, + cfnUserPool, +}); +backend.addOutput(customOutputs); + +cfnUserPool.schema = undefined; +cfnUserPool.usernameAttributes = []; +cfnUserPool.emailConfiguration = { + emailSendingAccount: "DEVELOPER", + from: "ktruon@amazon.com", + sourceArn: `arn:aws:ses:${stack.region}:${stack.account}:identity/ktruon@amazon.com`, +}; +cfnUserPool.adminCreateUserConfig = { + allowAdminCreateUserOnly: true, +}; +cfnUserPool.autoVerifiedAttributes = []; +cfnUserPool.userAttributeUpdateSettings = { + attributesRequireVerificationBeforeUpdate: [], +}; diff --git a/infra-gen2/backends/auth/username-login-mfa/amplify/package.json b/infra-gen2/backends/auth/username-login-mfa/amplify/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/infra-gen2/backends/auth/username-login-mfa/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/username-login-mfa/amplify/tsconfig.json b/infra-gen2/backends/auth/username-login-mfa/amplify/tsconfig.json new file mode 100644 index 0000000000..4eb4ab26ca --- /dev/null +++ b/infra-gen2/backends/auth/username-login-mfa/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/infra-gen2/backends/auth/username-login-mfa/package.json b/infra-gen2/backends/auth/username-login-mfa/package.json new file mode 100644 index 0000000000..e604512607 --- /dev/null +++ b/infra-gen2/backends/auth/username-login-mfa/package.json @@ -0,0 +1,5 @@ +{ + "name": "username-login-mfa", + "version": "1.0.0", + "main": "index.js" +} diff --git a/infra-gen2/package-lock.json b/infra-gen2/package-lock.json index 806c800dd6..f56c0348d5 100644 --- a/infra-gen2/package-lock.json +++ b/infra-gen2/package-lock.json @@ -45,9 +45,15 @@ "backends/auth/email-sign-in": { "version": "1.0.0" }, + "backends/auth/mfa-optional-email": { + "version": "1.0.0" + }, "backends/auth/mfa-optional-sms": { "version": "1.0.0" }, + "backends/auth/mfa-required-email": { + "version": "1.0.0" + }, "backends/auth/mfa-required-sms": { "version": "1.0.0" }, @@ -4338,35 +4344,11 @@ "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.624.0", - "@aws-sdk/client-sts": "3.624.0", - "@aws-sdk/core": "3.624.0", - "@aws-sdk/credential-provider-node": "3.624.0", - "@aws-sdk/middleware-host-header": "3.620.0", - "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.620.0", - "@aws-sdk/middleware-sdk-ec2": "3.622.0", - "@aws-sdk/middleware-user-agent": "3.620.0", - "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.637.0", "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", - "@smithy/url-parser": "^3.0.3", - "@smithy/util-base64": "^3.0.0", - "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.14", - "@smithy/util-defaults-mode-node": "^3.0.14", - "@smithy/util-endpoints": "^2.0.5", - "@smithy/util-middleware": "^3.0.3", - "@smithy/util-retry": "^3.0.3", - "@smithy/util-utf8": "^3.0.0", - "@smithy/util-waiter": "^3.1.2", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" @@ -4378,14 +4360,6 @@ "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.624.0", - "@aws-sdk/middleware-host-header": "3.620.0", - "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.620.0", - "@aws-sdk/middleware-user-agent": "3.620.0", - "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@smithy/node-config-provider": "^3.1.4", "@smithy/types": "^3.3.0", @@ -4498,8 +4472,6 @@ "license": "Apache-2.0", "dependencies": { "@smithy/types": "^3.3.0", - "@smithy/util-middleware": "^3.0.3", - "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { @@ -10505,6 +10477,20 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.614.0.tgz", + "integrity": "sha512-wK2cdrXHH4oz4IomV/yrGkftU9A+ITB6nFL+rxxyO78is2ifHJpFdV4aqk4LSkXYPi6CXWNru/Dqc7yiKXgJPw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-sdk/region-config-resolver": { "version": "3.614.0", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.614.0.tgz", @@ -10555,10 +10541,9 @@ } }, "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-sdk/util-endpoints": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.614.0.tgz", - "integrity": "sha512-wK2cdrXHH4oz4IomV/yrGkftU9A+ITB6nFL+rxxyO78is2ifHJpFdV4aqk4LSkXYPi6CXWNru/Dqc7yiKXgJPw==", - "license": "Apache-2.0", + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.637.0.tgz", + "integrity": "sha512-pAqOKUHeVWHEXXDIp/qoMk/6jyxIb6GGjnK1/f8dKHtKIEs4tKsnnL563gceEvdad53OPXIt86uoevCcCzmBnw==", "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/types": "^3.3.0", @@ -20121,10 +20106,18 @@ "node": ">= 8" } }, + "node_modules/mfa-optional-email": { + "resolved": "backends/auth/mfa-optional-email", + "link": true + }, "node_modules/mfa-optional-sms": { "resolved": "backends/auth/mfa-optional-sms", "link": true }, + "node_modules/mfa-required-email": { + "resolved": "backends/auth/mfa-required-email", + "link": true + }, "node_modules/mfa-required-sms": { "resolved": "backends/auth/mfa-required-sms", "link": true diff --git a/infra-gen2/tool/deploy_gen2.dart b/infra-gen2/tool/deploy_gen2.dart index e5f8cf1a1e..b83efb7c60 100644 --- a/infra-gen2/tool/deploy_gen2.dart +++ b/infra-gen2/tool/deploy_gen2.dart @@ -58,6 +58,41 @@ const List infraConfig = [ identifier: 'mfa-req-sms', pathToSource: 'infra-gen2/backends/auth/mfa-required-sms', ), + AmplifyBackend( + name: 'mfa-required-email', + identifier: 'mfa-req-email', + pathToSource: 'infra-gen2/backends/auth/mfa-required-email', + ), + AmplifyBackend( + name: 'mfa-required-email-sms', + identifier: 'mfa-req-ema-sms', + pathToSource: 'infra-gen2/backends/auth/mfa-required-email-sms', + ), + AmplifyBackend( + name: 'mfa-optional-email', + identifier: 'mfa-opt-email', + pathToSource: 'infra-gen2/backends/auth/mfa-optional-email', + ), + AmplifyBackend( + name: 'mfa-optional-email-sms', + identifier: 'mfa-opt-ema-sms', + pathToSource: 'infra-gen2/backends/auth/mfa-optional-email-sms', + ), + AmplifyBackend( + name: 'mfa-required-email-totp', + identifier: 'mfa-req-ema-tot', + pathToSource: 'infra-gen2/backends/auth/mfa-required-email-totp', + ), + AmplifyBackend( + name: 'mfa-optional-email-totp', + identifier: 'mfa-opt-ema-tot', + pathToSource: 'infra-gen2/backends/auth/mfa-optional-email-totp', + ), + AmplifyBackend( + name: 'username-login-mfa', + identifier: 'user-login-mfa', + pathToSource: 'infra-gen2/backends/auth/username-login-mfa', + ), ], ), AmplifyBackendGroup( diff --git a/packages/amplify_core/doc/lib/auth.dart b/packages/amplify_core/doc/lib/auth.dart index 79cc4e0858..d4f33ab4c7 100644 --- a/packages/amplify_core/doc/lib/auth.dart +++ b/packages/amplify_core/doc/lib/auth.dart @@ -146,7 +146,7 @@ Future _handleSignInResult(SignInResult result) async { _handleCodeDelivery(codeDeliveryDetails); // #enddocregion handle-confirm-signin-sms // #docregion handle-confirm-signin-email - case AuthSignInStep.confirmSignInWithEmailMfaCode: + case AuthSignInStep.confirmSignInWithOtpCode: final codeDeliveryDetails = result.nextStep.codeDeliveryDetails!; _handleCodeDelivery(codeDeliveryDetails); // #enddocregion handle-confirm-signin-email diff --git a/packages/amplify_core/lib/src/types/auth/sign_in/auth_next_sign_in_step.g.dart b/packages/amplify_core/lib/src/types/auth/sign_in/auth_next_sign_in_step.g.dart index addf8e6b35..6d480f1414 100644 --- a/packages/amplify_core/lib/src/types/auth/sign_in/auth_next_sign_in_step.g.dart +++ b/packages/amplify_core/lib/src/types/auth/sign_in/auth_next_sign_in_step.g.dart @@ -82,7 +82,7 @@ const _$AuthSignInStepEnumMap = { 'continueSignInWithEmailMfaSetup', AuthSignInStep.confirmSignInWithSmsMfaCode: 'confirmSignInWithSmsMfaCode', AuthSignInStep.confirmSignInWithTotpMfaCode: 'confirmSignInWithTotpMfaCode', - AuthSignInStep.confirmSignInWithEmailMfaCode: 'confirmSignInWithEmailMfaCode', + AuthSignInStep.confirmSignInWithOtpCode: 'confirmSignInWithOtpCode', AuthSignInStep.confirmSignInWithNewPassword: 'confirmSignInWithNewPassword', AuthSignInStep.confirmSignInWithCustomChallenge: 'confirmSignInWithCustomChallenge', diff --git a/packages/amplify_core/lib/src/types/auth/sign_in/auth_sign_in_step.dart b/packages/amplify_core/lib/src/types/auth/sign_in/auth_sign_in_step.dart index eef001a650..e63cb3a58f 100644 --- a/packages/amplify_core/lib/src/types/auth/sign_in/auth_sign_in_step.dart +++ b/packages/amplify_core/lib/src/types/auth/sign_in/auth_sign_in_step.dart @@ -29,7 +29,7 @@ enum AuthSignInStep { confirmSignInWithTotpMfaCode, /// The sign-in is not complete and must be confirmed with an email code. - confirmSignInWithEmailMfaCode, + confirmSignInWithOtpCode, /// The sign-in is not complete and must be confirmed with the user's new /// password. diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/main_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/main_test.dart index 192d73db19..4c2bcec0ba 100644 --- a/packages/auth/amplify_auth_cognito/example/integration_test/main_test.dart +++ b/packages/auth/amplify_auth_cognito/example/integration_test/main_test.dart @@ -15,11 +15,19 @@ import 'fetch_auth_session_test.dart' as fetch_auth_session_tests; import 'get_current_user_test.dart' as get_current_user_tests; import 'hosted_ui_webview_test.dart' as hosted_ui_webview_tests; import 'hub_events_test.dart' as hub_events_tests; +import 'mfa_email_optional_test.dart' as mfa_email_optional_tests; +import 'mfa_email_required_test.dart' as mfa_email_required_tests; +import 'mfa_email_totp_optional_test.dart' as mfa_email_totp_optional_tests; +import 'mfa_email_totp_required_test.dart' as mfa_email_totp_required_tests; +import 'mfa_sms_email_optional_test.dart' as mfa_sms_email_optional_tests; +import 'mfa_sms_email_required_test.dart' as mfa_sms_email_required_tests; import 'mfa_sms_test.dart' as mfa_sms_tests; import 'mfa_sms_totp_optional_test.dart' as mfa_sms_totp_optional_tests; import 'mfa_sms_totp_required_test.dart' as mfa_sms_totp_required_tests; import 'mfa_totp_optional_test.dart' as mfa_totp_optional_tests; import 'mfa_totp_required_test.dart' as mfa_totp_required_tests; +import 'mfa_username_login_required_test.dart' + as mfa_username_login_required_tests; import 'native_auth_bridge_test.dart' as native_auth_bridge_tests; import 'reset_password_test.dart' as reset_password_tests; import 'security_test.dart' as security_tests; @@ -36,6 +44,18 @@ void main() async { group('amplify_auth_cognito', () { asf_tests.main(); + mfa_username_login_required_tests.main(); + mfa_sms_tests.main(); + mfa_sms_totp_optional_tests.main(); + mfa_sms_totp_required_tests.main(); + mfa_totp_optional_tests.main(); + mfa_totp_required_tests.main(); + mfa_email_optional_tests.main(); + mfa_email_required_tests.main(); + mfa_sms_email_optional_tests.main(); + mfa_sms_email_required_tests.main(); + mfa_email_totp_optional_tests.main(); + mfa_email_totp_required_tests.main(); confirm_sign_in_tests.main(); confirm_sign_up_tests.main(); custom_auth_tests.main(); @@ -47,11 +67,6 @@ void main() async { get_current_user_tests.main(); hosted_ui_webview_tests.main(); hub_events_tests.main(); - mfa_sms_tests.main(); - mfa_sms_totp_optional_tests.main(); - mfa_sms_totp_required_tests.main(); - mfa_totp_optional_tests.main(); - mfa_totp_required_tests.main(); native_auth_bridge_tests.main(); reset_password_tests.main(); security_tests.main(); diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_optional_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_optional_test.dart new file mode 100644 index 0000000000..0fed40593b --- /dev/null +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_optional_test.dart @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; + +void main() { + testRunner.setupTests(); + + group('MFA (Email)', () { + testRunner.withEnvironment(mfaOptionalEmail, (env) { + asyncTest('can sign in with Email MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: true, + attributes: { + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is optional', + ).equals(AuthSignInStep.done); + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + Future signInWithEmail() async { + await signOutUser(assertComplete: true); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + final mfaSetupRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(mfaSetupRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + await signInWithEmail(); + await signInWithEmail(); + }); + }); + }); +} diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_required_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_required_test.dart new file mode 100644 index 0000000000..5cd4809ad6 --- /dev/null +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_required_test.dart @@ -0,0 +1,82 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; + +void main() { + testRunner.setupTests(); + + group('MFA (Email)', () { + testRunner.withEnvironment(mfaRequiredEmail, (env) { + asyncTest('can sign in with Email MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: true, + attributes: { + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + Future signInWithEmail() async { + await signOutUser(assertComplete: true); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + final mfaSetupRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(mfaSetupRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + await signInWithEmail(); + await signInWithEmail(); + }); + }); + }); +} diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_totp_optional_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_totp_optional_test.dart new file mode 100644 index 0000000000..16a9987e1c --- /dev/null +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_totp_optional_test.dart @@ -0,0 +1,490 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; + +void main() { + testRunner.setupTests(); + + group('MFA (EMAIL + TOTP)', () { + testRunner.withEnvironment(mfaOptionalEmailTotp, (env) { + asyncTest('can set up TOTP MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + // Create user with no phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + autoFillAttributes: false, + attributes: { + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + because: 'MFA is optional', + signInRes.nextStep.signInStep, + ).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()) + .equals(const UserMfaPreference()); + + await setUpTotp(); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.totp}, + preferred: MfaType.totp, + ), + ); + + Future signInWithTotp() async { + await signOutUser(assertComplete: true); + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'Once TOTP MFA is preferred, it is performed ' + 'on every sign-in attempt.', + ).equals(AuthSignInStep.confirmSignInWithTotpMfaCode); + check(signInRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.totp) + ..has((d) => d.destination, 'destination') + .equals(friendlyDeviceName); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await generateTotpCode(), + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + await signInWithTotp(); + await signInWithTotp(); + + await check( + because: 'TOTP can be disabled when optional', + cognitoPlugin.updateMfaPreference(totp: MfaPreference.disabled), + ).completes(); + + check( + because: 'Disabling TOTP should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference(enabled: {}, preferred: null), + ); + }); + + asyncTest('can select TOTP MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + // Create a user with an unverified phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is optional', + ).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()) + .equals(const UserMfaPreference()); + + await setUpTotp(); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.totp}, + preferred: MfaType.totp, + ), + ); + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.enabled, + totp: MfaPreference.enabled, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + { + await signOutUser(assertComplete: true); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.continueSignInWithMfaSelection); + check(signInRes.nextStep.allowedMfaTypes) + .isNotNull() + .deepEquals({MfaType.email, MfaType.totp}); + + final selectRes = await Amplify.Auth.confirmSignIn( + confirmationValue: 'TOTP', + ); + check(selectRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithTotpMfaCode); + check(selectRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.totp) + ..has((d) => d.destination, 'destination') + .equals(friendlyDeviceName); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await generateTotpCode(), + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + // Verify we can set TOTP as preferred and forego selection. + + await cognitoPlugin.updateMfaPreference( + totp: MfaPreference.preferred, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'TOTP should be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.totp, + ), + ); + + { + await signOutUser(assertComplete: true); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithTotpMfaCode); + check(signInRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.totp) + ..has((d) => d.destination, 'destination') + .equals(friendlyDeviceName); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await generateTotpCode(), + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify we can switch to EMAIL as preferred. + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.email, + ), + ); + + { + await signOutUser(assertComplete: true); + + final otpRes = await getOtpCode(env.getLoginAttribute(username)); + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpRes.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify marking enabled does not change preference. + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.enabled, + totp: MfaPreference.enabled, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'EMAIL should still be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.email, + ), + ); + + // Verify we can mark neither as preferred + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.notPreferred, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'EMAIL should be marked not preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + // Verify that we can disable both + await check( + because: 'MFA can be disabled when optional', + cognitoPlugin.updateMfaPreference( + email: MfaPreference.disabled, + totp: MfaPreference.disabled, + ), + ).completes(); + + check( + because: 'Disabling MFA should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference(enabled: {}, preferred: null), + ); + }); + + asyncTest('can select EMAIL MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + // Create a user with an unverified phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is optional', + ).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()) + .equals(const UserMfaPreference()); + + await setUpTotp(); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.totp}, + preferred: MfaType.totp, + ), + ); + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.enabled, + totp: MfaPreference.enabled, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + await signOutUser(assertComplete: true); + + final otpResult = await getOtpCode(UserAttribute.email(username)); + + final resignInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(resignInRes.nextStep.signInStep) + .equals(AuthSignInStep.continueSignInWithMfaSelection); + check(resignInRes.nextStep.allowedMfaTypes) + .isNotNull() + .deepEquals({MfaType.email, MfaType.totp}); + + final selectRes = await Amplify.Auth.confirmSignIn( + confirmationValue: 'EMAIL', + ); + check(selectRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(selectRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + // Verify we can set EMAIL as preferred and forego selection. + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.email, + ), + ); + + { + await signOutUser(assertComplete: true); + + final otpResult = await getOtpCode(UserAttribute.email(username)); + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify we can switch to TOTP as preferred. + + await cognitoPlugin.updateMfaPreference( + totp: MfaPreference.preferred, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'TOTP should be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.totp, + ), + ); + + { + await signOutUser(assertComplete: true); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithTotpMfaCode); + check(signInRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.totp) + ..has((d) => d.destination, 'destination') + .equals(friendlyDeviceName); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await generateTotpCode(), + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify marking enabled does not change preference. + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.enabled, + totp: MfaPreference.enabled, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'TOTP should still be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.totp, + ), + ); + + // Verify we can mark neither as preferred + await cognitoPlugin.updateMfaPreference( + totp: MfaPreference.notPreferred, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'TOTP should be marked not preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + // Verify that we can disable both + await check( + because: 'MFA can be disabled when optional', + cognitoPlugin.updateMfaPreference( + email: MfaPreference.disabled, + totp: MfaPreference.disabled, + ), + ).completes(); + + check( + because: 'Disabling MFA should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference(enabled: {}, preferred: null), + ); + }); + }); + }); +} diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_totp_required_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_totp_required_test.dart new file mode 100644 index 0000000000..b7f49da0e6 --- /dev/null +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_email_totp_required_test.dart @@ -0,0 +1,292 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; + +void main() { + testRunner.setupTests(); + + group('MFA (EMAIL + TOTP)', () { + testRunner.withEnvironment(mfaRequiredEmailTotp, (env) { + asyncTest('can set up EMAIL MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + final otpResult = await getOtpCode(UserAttribute.email(username)); + + // Create a user with no phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + autoFillAttributes: false, + verifyAttributes: false, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: + 'When an email is registered and the userpool has email MFA enabled, Cognito will automatically enable email MFA as the preferred MFA method.', + ).equals(AuthSignInStep.confirmSignInWithOtpCode); + + final setupRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(setupRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + await signOutUser(assertComplete: true); + + final otpResult2 = await getOtpCode(UserAttribute.email(username)); + + final resignInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(resignInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(resignInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult2.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + }); + + asyncTest('can select TOTP MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + final otpResult = await getOtpCode(UserAttribute.email(username)); + + // Create a user with an unverified phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.email: username, + }, + ); + + { + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: + 'MFA is required so Cognito automatically enables EMAIL MFA', + ).equals(AuthSignInStep.confirmSignInWithOtpCode); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + check( + await cognitoPlugin.fetchMfaPreference(), + because: + 'MFA is required so Cognito automatically enables EMAIL MFA, this is expected behavior', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + await setUpTotp(); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + await signOutUser(assertComplete: true); + + { + final resignInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + resignInRes.nextStep.signInStep, + because: 'Both EMAIL + TOTP are activated with no preference', + ).equals(AuthSignInStep.continueSignInWithMfaSelection); + check(resignInRes.nextStep.allowedMfaTypes) + .isNotNull() + .deepEquals({MfaType.email, MfaType.totp}); + + final selectRes = await Amplify.Auth.confirmSignIn( + confirmationValue: 'TOTP', + ); + check(selectRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithTotpMfaCode); + check(selectRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.totp) + ..has((d) => d.destination, 'destination') + .equals(friendlyDeviceName); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await generateTotpCode(), + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + // Verify we can set TOTP as preferred and forego selection. + + await cognitoPlugin.updateMfaPreference( + totp: MfaPreference.preferred, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.totp, + ), + ); + + { + await signOutUser(assertComplete: true); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'Preference is TOTP MFA now', + ).equals(AuthSignInStep.confirmSignInWithTotpMfaCode); + check(signInRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.totp) + ..has((d) => d.destination, 'destination') + .equals(friendlyDeviceName); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await generateTotpCode(), + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify we can switch to EMAIL as preferred. + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.email, + ), + ); + + { + await signOutUser(assertComplete: true); + + final otpResult = await getOtpCode(UserAttribute.email(username)); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'Preference is EMAIL MFA now', + ).equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify marking enabled does not change preference. + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.enabled, + totp: MfaPreference.enabled, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'EMAIL should still be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: MfaType.email, + ), + ); + + // Verify we can mark neither as preferred + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.notPreferred, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'EMAIL should be marked not preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + // Verify that we can disable MFA + { + await check( + because: 'Interestingly, Cognito does not throw and allows ' + 'MFA to be disabled even when required.', + cognitoPlugin.updateMfaPreference( + email: MfaPreference.disabled, + totp: MfaPreference.disabled, + ), + ).completes(); + + check( + because: 'Disabling MFA should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference( + enabled: {}, + preferred: null, + ), + ); + } + }); + }); + }); +} diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_email_optional_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_email_optional_test.dart new file mode 100644 index 0000000000..4be77b4f06 --- /dev/null +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_email_optional_test.dart @@ -0,0 +1,434 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; + +void main() { + testRunner.setupTests(); + + group('MFA (SMS + EMAIL)', () { + testRunner.withEnvironment(mfaOptionalEmailSms, (env) { + asyncTest('can set up EMAIL MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + // Create user with no phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + autoFillAttributes: false, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + because: 'MFA is optional', + signInRes.nextStep.signInStep, + ).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()) + .equals(const UserMfaPreference()); + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + Future signInWithEmail() async { + await signOutUser(assertComplete: true); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'Once Email MFA is preferred, it is performed ' + 'on every sign-in attempt.', + ).equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + await signInWithEmail(); + await signInWithEmail(); + + await check( + because: 'EMAIL can be disabled when optional', + cognitoPlugin.updateMfaPreference(email: MfaPreference.disabled), + ).completes(); + + check( + because: 'Disabling EMAIL should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference(enabled: {}, preferred: null), + ); + }); + + asyncTest('can select EMAIL MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + final phoneNumber = generatePhoneNumber(); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + // Create a user with an unverified phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.phoneNumber: phoneNumber, + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is optional', + ).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()) + .equals(const UserMfaPreference()); + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.enabled, + ); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + { + await signOutUser(assertComplete: true); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + // Verify we can switch to SMS as preferred. + + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.preferred, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.sms, + ), + ); + + { + await signOutUser(assertComplete: true); + + final mfaCode = await getOtpCode(UserAttribute.phone(phoneNumber)); + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithSmsMfaCode); + check(signInRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.sms) + ..has((d) => d.destination, 'destination') + .isNotNull() + .startsWith('+'); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await mfaCode.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify marking enabled does not change preference. + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.enabled, + email: MfaPreference.enabled, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'SMS should still be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.sms, + ), + ); + + // Verify that we can disable both + await check( + because: 'MFA can be disabled when optional', + cognitoPlugin.updateMfaPreference( + sms: MfaPreference.disabled, + email: MfaPreference.disabled, + ), + ).completes(); + + check( + because: 'Disabling MFA should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference(enabled: {}, preferred: null), + ); + }); + + asyncTest('can select SMS MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + final phoneNumber = generatePhoneNumber(); + + // Create a user with an unverified phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.phoneNumber: phoneNumber, + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is optional', + ).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()) + .equals(const UserMfaPreference()); + + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.preferred, + email: MfaPreference.enabled, + ); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.sms}, + preferred: MfaType.sms, + ), + ); + + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.enabled, + email: MfaPreference.enabled, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.sms, + ), + ); + + await signOutUser(assertComplete: true); + + final mfaCode = await getOtpCode(UserAttribute.phone(phoneNumber)); + + final resignInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(resignInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithSmsMfaCode); + check(resignInRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.sms) + ..has((d) => d.destination, 'destination') + .isNotNull() + .startsWith('+'); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await mfaCode.code, + ); + + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.sms, + ), + ); + + // Verify we can set SMS as preferred and forego selection. + + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.preferred, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.sms, + ), + ); + + { + await signOutUser(assertComplete: true); + + final mfaCode = await getOtpCode(UserAttribute.phone(phoneNumber)); + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithSmsMfaCode); + check(signInRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.sms) + ..has((d) => d.destination, 'destination') + .isNotNull() + .startsWith('+'); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await mfaCode.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify we can switch to EMAIL as preferred. + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'EMAIL should be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + { + await signOutUser(assertComplete: true); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + + check(signInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify marking enabled does not change preference. + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.enabled, + email: MfaPreference.enabled, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'EMAIL should still be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + // Verify that we can disable both + await check( + because: 'MFA can be disabled when optional', + cognitoPlugin.updateMfaPreference( + sms: MfaPreference.disabled, + email: MfaPreference.disabled, + ), + ).completes(); + + check( + because: 'Disabling MFA should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference(enabled: {}, preferred: null), + ); + }); + }); + }); +} diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_email_required_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_email_required_test.dart new file mode 100644 index 0000000000..6a3da16919 --- /dev/null +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_sms_email_required_test.dart @@ -0,0 +1,354 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; + +void main() { + testRunner.setupTests(); + + group('MFA (EMAIL + SMS)', () { + testRunner.withEnvironment(mfaRequiredEmailSms, (env) { + asyncTest('can set up EMAIL MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + // Create a user with no phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + attributes: { + AuthUserAttributeKey.email: username, + }, + autoFillAttributes: false, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is required, and EMAIL is chosen when ' + 'no phone number is registered', + ).equals(AuthSignInStep.confirmSignInWithOtpCode); + + final setupRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(setupRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + await signOutUser(assertComplete: true); + + // Verify we can sign in with EMAIL MFA as the preferred method and forego selection. + + final otpResult2 = await getOtpCode( + env.getLoginAttribute(username), + ); + + final resignInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(resignInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(resignInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult2.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + }); + + asyncTest('can select EMAIL MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + final phoneNumber = generatePhoneNumber(); + + final mfaCode = await getOtpCode(UserAttribute.phone(phoneNumber)); + + // Verify we can set EMAIL as preferred and forego selection. + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.phoneNumber: phoneNumber, + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is required so Cognito automatically enables SMS MFA', + ).equals(AuthSignInStep.confirmSignInWithSmsMfaCode); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await mfaCode.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'MFA is required so Cognito automatically enables SMS MFA', + ).equals( + const UserMfaPreference( + enabled: {MfaType.sms}, + preferred: MfaType.sms, + ), + ); + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + await signOutUser(assertComplete: true); + + { + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + final resignInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + resignInRes.nextStep.signInStep, + because: 'Preference is EMAIL MFA now', + ).equals(AuthSignInStep.confirmSignInWithOtpCode); + check(resignInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + // Verify marking enabled does not change preference. + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.enabled, + email: MfaPreference.enabled, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'SMS should still be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + // Verify that we can disable MFA + { + await check( + because: 'Interestingly, Cognito does not throw and allows ' + 'MFA to be disabled even when required.', + cognitoPlugin.updateMfaPreference( + sms: MfaPreference.disabled, + email: MfaPreference.disabled, + ), + ).completes(); + + check( + because: 'Disabling MFA should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference( + enabled: {}, + preferred: null, + ), + ); + } + }); + + asyncTest('can select SMS MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + final phoneNumber = generatePhoneNumber(); + + final mfaCode = await getOtpCode(UserAttribute.phone(phoneNumber)); + + // Create a user with an unverified phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.phoneNumber: phoneNumber, + AuthUserAttributeKey.email: username, + }, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is required so Cognito automatically enables SMS MFA', + ).equals(AuthSignInStep.confirmSignInWithSmsMfaCode); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await mfaCode.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'MFA is required so Cognito automatically enables SMS MFA', + ).equals( + const UserMfaPreference( + enabled: {MfaType.sms}, + preferred: MfaType.sms, + ), + ); + + // Verify we can set SMS as preferred and forego selection. + + { + await signOutUser(assertComplete: true); + + final mfaCode = await getOtpCode(UserAttribute.phone(phoneNumber)); + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'Preference is SMS MFA now', + ).equals(AuthSignInStep.confirmSignInWithSmsMfaCode); + check(signInRes.nextStep.codeDeliveryDetails).isNotNull() + ..has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.sms) + ..has((d) => d.destination, 'destination') + .isNotNull() + .startsWith('+'); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await mfaCode.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify we can switch to EMAIL as preferred. + + await cognitoPlugin.updateMfaPreference( + email: MfaPreference.preferred, + ); + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + { + await signOutUser(assertComplete: true); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'Preference is EMAIL MFA now', + ).equals(AuthSignInStep.confirmSignInWithOtpCode); + check(signInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + // Verify marking enabled does not change preference. + await cognitoPlugin.updateMfaPreference( + sms: MfaPreference.enabled, + email: MfaPreference.enabled, + ); + check( + await cognitoPlugin.fetchMfaPreference(), + because: 'EMAIL should still be marked preferred', + ).equals( + const UserMfaPreference( + enabled: {MfaType.sms, MfaType.email}, + preferred: MfaType.email, + ), + ); + + // Verify that we can disable MFA + { + await check( + because: 'Interestingly, Cognito does not throw and allows ' + 'MFA to be disabled even when required.', + cognitoPlugin.updateMfaPreference( + sms: MfaPreference.disabled, + email: MfaPreference.disabled, + ), + ).completes(); + + check( + because: 'Disabling MFA should mark it as not preferred', + await cognitoPlugin.fetchMfaPreference(), + ).equals( + const UserMfaPreference( + enabled: {}, + preferred: null, + ), + ); + } + }); + }); + }); +} diff --git a/packages/auth/amplify_auth_cognito/example/integration_test/mfa_username_login_required_test.dart b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_username_login_required_test.dart new file mode 100644 index 0000000000..9903ed6e2d --- /dev/null +++ b/packages/auth/amplify_auth_cognito/example/integration_test/mfa_username_login_required_test.dart @@ -0,0 +1,247 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; + +void main() { + testRunner.setupTests(); + + group('MFA (EMAIL + TOTP + SMS)', () { + testRunner.withEnvironment(mfaRequiredUsernameLogin, (env) { + asyncTest('can set up EMAIL MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + final email = generateEmail(); + + final otpResult = await getOtpCode(UserAttribute.email(email)); + + // Create a user with no phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + autoFillAttributes: false, + verifyAttributes: false, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + + check( + signInRes.nextStep.signInStep, + because: + 'When an email is registered and the userpool has email MFA enabled, Cognito will automatically enable email MFA as the preferred MFA method.', + ).equals(AuthSignInStep.continueSignInWithMfaSetupSelection); + + await Amplify.Auth.confirmSignIn( + confirmationValue: 'EMAIL', + ); + + await Amplify.Auth.confirmSignIn( + confirmationValue: email, + ); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + await signOutUser(assertComplete: true); + + final otpResult2 = await getOtpCode(UserAttribute.email(email)); + + final resignInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check(resignInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(resignInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes2 = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult2.code, + ); + check(confirmRes2.nextStep.signInStep).equals(AuthSignInStep.done); + }); + + asyncTest('can setup TOTP MFA', (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + + // Create a user with an unverified phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + autoFillAttributes: false, + ); + + { + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + check( + signInRes.nextStep.signInStep, + because: 'MFA is required so users select a method to setup', + ).equals(AuthSignInStep.continueSignInWithMfaSetupSelection); + + final selectRes = await Amplify.Auth.confirmSignIn( + confirmationValue: 'TOTP', + ); + + final sharedSecret = + selectRes.nextStep.totpSetupDetails!.sharedSecret; + final setupRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await generateTotpCode(sharedSecret), + options: const ConfirmSignInOptions( + pluginOptions: CognitoConfirmSignInPluginOptions( + friendlyDeviceName: friendlyDeviceName, + ), + ), + ); + + check(setupRes.nextStep.signInStep).equals(AuthSignInStep.done); + } + + check( + await cognitoPlugin.fetchMfaPreference(), + because: + 'MFA is required so Cognito automatically enables EMAIL MFA, this is expected behavior', + ).equals( + const UserMfaPreference( + enabled: {MfaType.totp}, + preferred: MfaType.totp, + ), + ); + + await signOutUser(assertComplete: true); + }); + + asyncTest( + 'Can set up EMAIL and TOTP MFA and then choose a preferred method', + (_) async { + final username = env.generateUsername(); + final password = generatePassword(); + final email = generateEmail(); + + final otpResult = await getOtpCode(UserAttribute.email(email)); + + // Create a user with no phone number. + await adminCreateUser( + username, + password, + autoConfirm: true, + autoFillAttributes: false, + verifyAttributes: false, + ); + + final signInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + + check( + signInRes.nextStep.signInStep, + because: + 'When both EMAIL and TOTP are enabled but email attribute isnt verified, choose an mfa method to set up.', + ).equals(AuthSignInStep.continueSignInWithMfaSetupSelection); + + await Amplify.Auth.confirmSignIn( + confirmationValue: 'EMAIL', + ); + + await Amplify.Auth.confirmSignIn( + confirmationValue: email, + ); + + final confirmRes = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult.code, + ); + + check(confirmRes.nextStep.signInStep).equals(AuthSignInStep.done); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email}, + preferred: MfaType.email, + ), + ); + + await signOutUser(assertComplete: true); + + final otpResult2 = await getOtpCode(UserAttribute.email(email)); + + final resignInRes = await Amplify.Auth.signIn( + username: username, + password: password, + ); + + check(resignInRes.nextStep.signInStep) + .equals(AuthSignInStep.confirmSignInWithOtpCode); + check(resignInRes.nextStep.codeDeliveryDetails) + .isNotNull() + .has((d) => d.deliveryMedium, 'deliveryMedium') + .equals(DeliveryMedium.email); + + final confirmRes2 = await Amplify.Auth.confirmSignIn( + confirmationValue: await otpResult2.code, + ); + check(confirmRes2.nextStep.signInStep).equals(AuthSignInStep.done); + + await setUpTotp(); + + check(await cognitoPlugin.fetchMfaPreference()).equals( + const UserMfaPreference( + enabled: {MfaType.email, MfaType.totp}, + preferred: null, + ), + ); + + // sign out and sign back in and confirm TOTP + await signOutUser(assertComplete: true); + + final resignInRes2 = await Amplify.Auth.signIn( + username: username, + password: password, + ); + + check(resignInRes2.nextStep.signInStep) + .equals(AuthSignInStep.continueSignInWithMfaSelection); + + // select totp as the preferred method + await Amplify.Auth.confirmSignIn( + confirmationValue: 'TOTP', + ); + + final confirmRes3 = await Amplify.Auth.confirmSignIn( + confirmationValue: await generateTotpCode(), + ); + + check(confirmRes3.nextStep.signInStep).equals(AuthSignInStep.done); + }, + ); + }); + }); +} diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/flows/constants.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/flows/constants.dart index 361ac6ccb8..174bf2c9bf 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/flows/constants.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/flows/constants.dart @@ -61,7 +61,7 @@ abstract class CognitoConstants { static const challengeParamSmsMfaCode = 'SMS_MFA_CODE'; /// The `EMAIL_OTP_CODE` parameter. - static const challengeParamEmailMfaCode = 'EMAIL_OTP_CODE'; + static const challengeParamEmailOtpCode = 'EMAIL_OTP_CODE'; /// The `SOFTWARE_TOKEN_MFA_CODE` parameter. static const challengeParamSoftwareTokenMfaCode = 'SOFTWARE_TOKEN_MFA_CODE'; diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/sdk/sdk_bridge.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/sdk/sdk_bridge.dart index a48c8d4a01..9b4de91f08 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/sdk/sdk_bridge.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/sdk/sdk_bridge.dart @@ -32,8 +32,7 @@ extension ChallengeNameTypeBridge on ChallengeNameType { AuthSignInStep.continueSignInWithMfaSetupSelection, ChallengeNameType.softwareTokenMfa => AuthSignInStep.confirmSignInWithTotpMfaCode, - ChallengeNameType.emailOtp => - AuthSignInStep.confirmSignInWithEmailMfaCode, + ChallengeNameType.emailOtp => AuthSignInStep.confirmSignInWithOtpCode, ChallengeNameType.adminNoSrpAuth || ChallengeNameType.passwordVerifier || ChallengeNameType.devicePasswordVerifier || diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_in_state_machine.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_in_state_machine.dart index 2fdb6c66af..61a725e683 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_in_state_machine.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/sign_in_state_machine.dart @@ -327,7 +327,7 @@ final class SignInStateMachine ChallengeNameType.softwareTokenMfa when hasUserResponse => createSoftwareTokenMfaRequest(event), ChallengeNameType.emailOtp when hasUserResponse => - createEmailMfaRequest(event), + createEmailOtpRequest(event), ChallengeNameType.selectMfaType when hasUserResponse => createSelectMfaRequest(event), ChallengeNameType.mfaSetup when hasUserResponse => @@ -454,7 +454,7 @@ final class SignInStateMachine /// Creates the response object for an Email MFA challenge. @protected - Future createEmailMfaRequest( + Future createEmailOtpRequest( SignInRespondToChallenge event, ) async { _enableMfaType = MfaType.email; @@ -464,7 +464,7 @@ final class SignInStateMachine ..challengeName = _challengeName ..challengeResponses.addAll({ CognitoConstants.challengeParamUsername: cognitoUsername, - CognitoConstants.challengeParamEmailMfaCode: event.answer, + CognitoConstants.challengeParamEmailOtpCode: event.answer, }) ..clientMetadata.addAll(event.clientMetadata); }); @@ -702,13 +702,13 @@ final class SignInStateMachine // User has provided the verification code return _enableMfaType == MfaType.totp - ? createMfaSetupRequest(event) + ? createTotpMfaSetupRequest(event) : createEmailMfaSetupRequest(event); } /// Completes set up of a TOTP MFA. @protected - Future createMfaSetupRequest( + Future createTotpMfaSetupRequest( SignInRespondToChallenge event, ) async { await verifySoftwareToken( diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/main_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/main_test.dart index fa1cbc1f8a..2fd7c73508 100644 --- a/packages/authenticator/amplify_authenticator/example/integration_test/main_test.dart +++ b/packages/authenticator/amplify_authenticator/example/integration_test/main_test.dart @@ -13,9 +13,14 @@ import 'http_test.dart' as http_tests; import 'reset_password_test.dart' as reset_password_tests; import 'sign_in_force_new_password_test.dart' as sign_in_force_new_password_tests; +import 'sign_in_mfa_email_test.dart' as sign_in_mfa_email_tests; +import 'sign_in_mfa_email_totp_test.dart' as sign_in_mfa_email_totp_tests; +import 'sign_in_mfa_sms_email_test.dart' as sign_in_mfa_sms_email_tests; import 'sign_in_mfa_sms_test.dart' as sign_in_mfa_sms_tests; import 'sign_in_mfa_sms_totp_test.dart' as sign_in_mfa_sms_totp_tests; import 'sign_in_mfa_totp_test.dart' as sign_in_mfa_totp_tests; +import 'sign_in_mfa_username_login_test.dart' + as sign_in_mfa_username_login_tests; import 'sign_in_with_email_test.dart' as sign_in_with_email_tests; import 'sign_in_with_phone_test.dart' as sign_in_with_phone_tests; import 'sign_in_with_username_test.dart' as sign_in_with_username_tests; @@ -45,6 +50,10 @@ void main() { sign_in_with_email_tests.main(); sign_in_with_phone_tests.main(); sign_in_with_username_tests.main(); + sign_in_mfa_email_tests.main(); + sign_in_mfa_email_totp_tests.main(); + sign_in_mfa_sms_email_tests.main(); + sign_in_mfa_username_login_tests.main(); sign_out_tests.main(); sign_up_with_email_tests.main(); sign_up_with_email_with_lambda_trigger_tests.main(); diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_email_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_email_test.dart new file mode 100644 index 0000000000..f7c6986312 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_email_test.dart @@ -0,0 +1,165 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_authenticator_test/amplify_authenticator_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; +import 'utils/test_utils.dart'; + +void main() { + testRunner.setupTests(); + + group('sign-in-email-mfa', () { + testRunner.withEnvironment(mfaRequiredEmail, (env) { + // Scenario: Sign in using a valid email MFA code + testWidgets('Sign in with valid EMAIL MFA code', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + attributes: { + AuthUserAttributeKey.email: username, + }, + autoFillAttributes: false, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInWithOtpCode, + isA(), + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInWithOtpCode, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + final otpResult = await getOtpCode( + env.getLoginAttribute(username), + ); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the email MFA code page + await confirmSignInPage.expectConfirmSignInWithOtpCodeIsPresent(); + + // And I type a valid EMAIL OTP code + await confirmSignInPage.enterVerificationCode(await otpResult.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + // When I sign out using Auth.signOut() + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + final otpResult2 = await getOtpCode( + env.getLoginAttribute(username), + ); + + // Then I see the sign in page + signInPage.expectUsername(label: 'Email'); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the EMAIL OTP code page + await confirmSignInPage.expectConfirmSignInWithOtpCodeIsPresent(); + + // When I type a valid EMAIL OTP code + await confirmSignInPage.enterVerificationCode(await otpResult2.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + + // Scenario: Sign in using an invalid email MFA code + testWidgets('Sign in with invalid EMAIL MFA code', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + attributes: { + AuthUserAttributeKey.email: username, + }, + autoFillAttributes: false, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInWithOtpCode, + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the EMAIL OTP code page + await confirmSignInPage.expectConfirmSignInWithOtpCodeIsPresent(); + + // And I type an invalid confirmation code + await confirmSignInPage.enterVerificationCode('123456'); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see "The code entered is not correct." + confirmSignInPage.expectInvalidVerificationCode(); + + await tester.bloc.close(); + }); + }); + }); +} diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_email_totp_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_email_totp_test.dart new file mode 100644 index 0000000000..fcee99259c --- /dev/null +++ b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_email_totp_test.dart @@ -0,0 +1,223 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_authenticator_test/amplify_authenticator_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; +import 'utils/test_utils.dart'; + +void main() { + testRunner.setupTests(); + + group('sign-in-email-totp-mfa', () { + testRunner.withEnvironment(mfaRequiredEmailTotp, (env) { + // Scenario: Sign in using a totp code when both EMAIL and TOTP are enabled + testWidgets('can select TOTP MFA', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.email: username, + }, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInWithOtpCode, + isA(), + UnauthenticatedState.signIn, + isA(), + UnauthenticatedState.confirmSignInWithTotpMfaCode, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + final otpResult = await getOtpCode(UserAttribute.email(username)); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the confirm email mfa page + await confirmSignInPage.expectConfirmSignInWithOtpCodeIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await otpResult.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + // When I enable TOTP for MFA instead of the default set up by cognito (EMAIL) + await setUpTotp(); + + // And I sign out using Auth.signOut() + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + // Then I see the sign in page + signInPage.expectEmail(); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the MFA selection page + await confirmSignInPage.expectConfirmSignInMfaSelectionIsPresent(); + + // When I select "TOTP" + await confirmSignInPage.selectMfaMethod(mfaMethod: MfaType.totp); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignInMfaSelection(); + + // Then I will be redirected to the TOTP MFA code page + await confirmSignInPage.expectConfirmSignInWithTotpMfaCodeIsPresent(); + + final code_2 = await generateTotpCode(); + + // When I type a valid TOTP code + await confirmSignInPage.enterVerificationCode(code_2); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + + // Scenario: Sign in using a EMAIL code when both EMAIL and TOTP are enabled + testWidgets('can select EMAIL MFA', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.email: username, + }, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInWithOtpCode, + isA(), + UnauthenticatedState.signIn, + isA(), + UnauthenticatedState.confirmSignInWithOtpCode, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + final otpResult = await getOtpCode(UserAttribute.email(username)); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the confirm email mfa page + await confirmSignInPage.expectConfirmSignInWithOtpCodeIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await otpResult.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + // When I enable TOTP for MFA instead of the default set up by cognito (EMAIL) + await setUpTotp(); + + // And I sign out using Auth.signOut() + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + // Then I see the sign in page + signInPage.expectEmail(); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the MFA selection page + await confirmSignInPage.expectConfirmSignInMfaSelectionIsPresent(); + + final otpResult2 = await getOtpCode(UserAttribute.email(username)); + + // When I select "EMAIL" + await confirmSignInPage.selectMfaMethod(mfaMethod: MfaType.email); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignInMfaSelection(); + + // Then I will be redirected to the confirm EMAIL mfa page + await confirmSignInPage.expectConfirmSignInWithOtpCodeIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await otpResult2.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + }); + }); +} diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_sms_email_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_sms_email_test.dart new file mode 100644 index 0000000000..c01460f6d5 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_sms_email_test.dart @@ -0,0 +1,218 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_authenticator_test/amplify_authenticator_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; +import 'utils/test_utils.dart'; + +void main() { + testRunner.setupTests(); + + group('sign-in-sms-totp-mfa', () { + testRunner.withEnvironment(mfaRequiredEmailSms, (env) { + // Scenario: Sign in using a totp code when both SMS and EMAIL are enabled + // Note: When email and sms are both enabled, + // one of them must be selected as preferred. + // This is different from other mfa methods and + // is expected behavior from cognito + testWidgets('can select EMAIL MFA', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + final phoneNumber = generateUSPhoneNumber(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.phoneNumber: phoneNumber.toE164(), + AuthUserAttributeKey.email: username, + }, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInMfa, + isA(), + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInWithOtpCode, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + final smsResult = + await getOtpCode(UserAttribute.phone(phoneNumber.toE164())); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the confirm sms mfa page + await confirmSignInPage.expectConfirmSignInMFAIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await smsResult.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + // When I enable EMAIL for MFA instead of the default set up by cognito (SMS) + await setUpEmailMfa(); + + // And I sign out using Auth.signOut() + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + final code_2 = await getOtpCode(env.getLoginAttribute(username)); + + // Then I see the sign in page + signInPage.expectEmail(); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the EMAIL MFA code page + await confirmSignInPage.expectConfirmSignInWithOtpCodeIsPresent(); + + // When I type a valid EMAIL MFA code + await confirmSignInPage.enterVerificationCode(await code_2.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + + // Scenario: Sign in using a SMS code when both SMS and TOTP are enabled + testWidgets('can select SMS MFA', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + final phoneNumber = generateUSPhoneNumber(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + attributes: { + AuthUserAttributeKey.phoneNumber: phoneNumber.toE164(), + AuthUserAttributeKey.email: username, + }, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInMfa, + isA(), + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInMfa, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + final smsResult_1 = + await getOtpCode(UserAttribute.phone(phoneNumber.toE164())); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the confirm sms mfa page + await confirmSignInPage.expectConfirmSignInMFAIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await smsResult_1.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + // When I enable EMAIL for MFA instead of the default set up by cognito (SMS) + await setUpEmailMfa(); + + final plugin = Amplify.Auth.getPlugin(AmplifyAuthCognito.pluginKey); + await plugin.updateMfaPreference(sms: MfaPreference.preferred); + + // And I sign out using Auth.signOut() + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + final smsResult_2 = + await getOtpCode(UserAttribute.phone(phoneNumber.toE164())); + + // Then I see the sign in page + signInPage.expectEmail(); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the confirm sms mfa page + await confirmSignInPage.expectConfirmSignInMFAIsPresent(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await smsResult_2.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + }); + }); +} diff --git a/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_username_login_test.dart b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_username_login_test.dart new file mode 100644 index 0000000000..823fa1ee51 --- /dev/null +++ b/packages/authenticator/amplify_authenticator/example/integration_test/sign_in_mfa_username_login_test.dart @@ -0,0 +1,260 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_integration_test/amplify_auth_integration_test.dart'; +import 'package:amplify_authenticator_test/amplify_authenticator_test.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:amplify_integration_test/amplify_integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_runner.dart'; +import 'utils/test_utils.dart'; + +void main() { + testRunner.setupTests(); + + group('sign-in-email-totp-mfa', () { + testRunner.withEnvironment(mfaRequiredUsernameLogin, (env) { + // Scenario: Select EMAIL MFA to set up from the setup selection page + testWidgets('can select EMAIL MFA to set up', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + final email = generateEmail(); + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + autoFillAttributes: false, + ); + + await loadAuthenticator(tester: tester); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + isA(), + UnauthenticatedState.continueSignInWithEmailMfaSetup, + UnauthenticatedState.confirmSignInWithOtpCode, + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + final emailMfaSetupPage = EmailMfaSetupPage(tester: tester); + + final otpResult = await getOtpCode(UserAttribute.email(email)); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the MFA setup selection page + await confirmSignInPage + .expectContinueSignInWithMfaSetupSelectionIsPresent(); + + // When I select "EMAIL" + await confirmSignInPage.selectMfaSetupMethod(mfaMethod: MfaType.email); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignInMfaSetupSelection(); + + // Then I will be redirected to the EMAIL MFA setup page + await emailMfaSetupPage.expectEmailMfaSetupIsPresent(); + + // When I type a valid email + await emailMfaSetupPage.enterEmail(email); + + // And I click the "Confirm" button + await emailMfaSetupPage.submitEmail(); + + // When I type a valid confirmation code + await confirmSignInPage.enterVerificationCode(await otpResult.code); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + + // Scenario: Select TOTP MFA to set up from the setup selection page + testWidgets('can select TOTP MFA to set up', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + late String sharedSecret; + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + autoFillAttributes: false, + ); + + await loadAuthenticator(tester: tester); + + tester.bloc.stream.listen((event) { + if (event is ContinueSignInTotpSetup) { + sharedSecret = event.totpSetupDetails.sharedSecret; + } + }); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + isA(), + isA(), + isA(), + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the MFA setup selection page + await confirmSignInPage + .expectContinueSignInWithMfaSetupSelectionIsPresent(); + + // When I select "TOTP" + await confirmSignInPage.selectMfaSetupMethod(mfaMethod: MfaType.totp); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignInMfaSetupSelection(); + + // Then I will be redirected to the TOTP MFA setup page + await confirmSignInPage.expectSignInTotpSetupIsPresent(); + + final totpCode = await generateTotpCode(sharedSecret); + + // When I type a valid TOTP code + await confirmSignInPage.enterVerificationCode(totpCode); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + await tester.bloc.close(); + }); + + // Scenario: Sign in using an invalid TOTP code + testWidgets('sign in with invalid TOTP code', (tester) async { + final username = env.generateUsername(); + final password = generatePassword(); + late String sharedSecret; + + await adminCreateUser( + username, + password, + autoConfirm: true, + verifyAttributes: false, + autoFillAttributes: false, + ); + + await loadAuthenticator(tester: tester); + + tester.bloc.stream.listen((event) { + if (event is ContinueSignInTotpSetup) { + sharedSecret = event.totpSetupDetails.sharedSecret; + } + }); + + expect( + tester.bloc.stream, + emitsInOrder([ + UnauthenticatedState.signIn, + isA(), + isA(), + isA(), + UnauthenticatedState.signIn, + UnauthenticatedState.confirmSignInWithTotpMfaCode, + emitsDone, + ]), + ); + + final signInPage = SignInPage(tester: tester); + final confirmSignInPage = ConfirmSignInPage(tester: tester); + + // When I type my "username" + await signInPage.enterUsername(username); + + // And I type my password + await signInPage.enterPassword(password); + + // And I click the "Sign in" button + await signInPage.submitSignIn(); + + // Then I will be redirected to the MFA setup selection page + await confirmSignInPage + .expectContinueSignInWithMfaSetupSelectionIsPresent(); + + // When I select "TOTP" + await confirmSignInPage.selectMfaSetupMethod(mfaMethod: MfaType.totp); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignInMfaSetupSelection(); + + // Then I will be redirected to the TOTP MFA setup page + await confirmSignInPage.expectSignInTotpSetupIsPresent(); + + final totpCode = await generateTotpCode(sharedSecret); + + // When I type a valid TOTP code + await confirmSignInPage.enterVerificationCode(totpCode); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see the authenticated app + await signInPage.expectAuthenticated(); + + // Sign out to test invalid TOTP code during sign-in + await Amplify.Auth.signOut(); + await tester.pumpAndSettle(); + + // When I attempt to sign in again + await signInPage.enterUsername(username); + await signInPage.enterPassword(password); + await signInPage.submitSignIn(); + + // Then I will be redirected to the TOTP MFA code page + await confirmSignInPage.expectConfirmSignInWithTotpMfaCodeIsPresent(); + + // When I type an invalid TOTP code + await confirmSignInPage.enterVerificationCode('000000'); + + // And I click the "Confirm" button + await confirmSignInPage.submitConfirmSignIn(); + + // Then I see "Invalid code" error message + confirmSignInPage.expectInvalidVerificationCode(); + + await tester.bloc.close(); + }); + }); + }); +} diff --git a/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart b/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart index 55abb9c220..81d94d88a8 100644 --- a/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart +++ b/packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart @@ -719,7 +719,7 @@ class _AuthenticatorState extends State { continueSignInWithEmailMfaSetupForm: ContinueSignInWithEmailMfaSetupForm(), confirmSignInWithTotpMfaCodeForm: ConfirmSignInMFAForm(), - confirmSignInWithEmailMfaCodeForm: ConfirmSignInMFAForm(), + confirmSignInWithOtpCodeForm: ConfirmSignInMFAForm(), verifyUserForm: VerifyUserForm(), confirmVerifyUserForm: ConfirmVerifyUserForm(), child: widget.child, diff --git a/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart b/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart index 353db05de4..00c6eb222d 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart @@ -227,8 +227,8 @@ class StateMachineBloc yield UnauthenticatedState.confirmSignInNewPassword; case AuthSignInStep.confirmSignInWithTotpMfaCode: yield UnauthenticatedState.confirmSignInWithTotpMfaCode; - case AuthSignInStep.confirmSignInWithEmailMfaCode: - yield UnauthenticatedState.confirmSignInWithEmailMfaCode; + case AuthSignInStep.confirmSignInWithOtpCode: + yield UnauthenticatedState.confirmSignInWithOtpCode; case AuthSignInStep.continueSignInWithMfaSelection: yield ContinueSignInWithMfaSelection( allowedMfaTypes: result.nextStep.allowedMfaTypes, @@ -345,9 +345,9 @@ class StateMachineBloc _emit(UnauthenticatedState.continueSignInWithEmailMfaSetup); case AuthSignInStep.confirmSignInWithTotpMfaCode: _emit(UnauthenticatedState.confirmSignInWithTotpMfaCode); - case AuthSignInStep.confirmSignInWithEmailMfaCode: + case AuthSignInStep.confirmSignInWithOtpCode: _notifyCodeSent(result.nextStep.codeDeliveryDetails?.destination); - _emit(UnauthenticatedState.confirmSignInWithEmailMfaCode); + _emit(UnauthenticatedState.confirmSignInWithOtpCode); case AuthSignInStep.resetPassword: _emit(UnauthenticatedState.confirmResetPassword); case AuthSignInStep.confirmSignUp: @@ -543,7 +543,9 @@ class StateMachineBloc yield* const Stream.empty(); } - Future _handleMfaSetupSelection(SignInResult result) async { + Future _handleMfaSetupSelection( + SignInResult result, + ) async { final allowedMfaTypes = result.nextStep.allowedMfaTypes; if (allowedMfaTypes == null) { @@ -556,7 +558,9 @@ class StateMachineBloc final mfaTypesForSetup = allowedMfaTypes.toSet()..remove(MfaType.sms); if (mfaTypesForSetup.length != 1) { - return ContinueSignInWithMfaSetupSelection(allowedMfaTypes: allowedMfaTypes); + return ContinueSignInWithMfaSetupSelection( + allowedMfaTypes: allowedMfaTypes, + ); } final mfaType = mfaTypesForSetup.first; diff --git a/packages/authenticator/amplify_authenticator/lib/src/enums/authenticator_step.dart b/packages/authenticator/amplify_authenticator/lib/src/enums/authenticator_step.dart index 5ef325081a..8fcc406563 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/enums/authenticator_step.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/enums/authenticator_step.dart @@ -82,7 +82,7 @@ enum AuthenticatorStep { confirmSignInWithTotpMfaCode, /// The sign-in is not complete and must be confirmed with an email code. - confirmSignInWithEmailMfaCode, + confirmSignInWithOtpCode, /// The user is on the Reset Password step. resetPassword, diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart index 852f8e4d7b..abe2edf4ce 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart @@ -145,7 +145,7 @@ abstract class AuthenticatorTitleLocalizations { /// /// In en, this message translates to: /// **'Enter your one-time passcode'** - String get confirmSignInWithEmailMfaCode; + String get confirmSignInWithOtpCode; /// Title of the Continue Sign In with Email MFA Setup step and form /// diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart index 692f579fa5..938f974e9f 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart @@ -31,7 +31,7 @@ class AuthenticatorTitleLocalizationsEn String get confirmSignInWithTotpMfaCode => 'Enter your one-time passcode'; @override - String get confirmSignInWithEmailMfaCode => 'Enter your one-time passcode'; + String get confirmSignInWithOtpCode => 'Enter your one-time passcode'; @override String get continueSignInWithEmailMfaSetup => diff --git a/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart b/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart index e5c1b19480..318753c227 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart @@ -50,9 +50,9 @@ class TitleResolver extends Resolver { } /// The title for the confirm sign in (email MFA code) Widget. - String confirmSignInWithEmailMfaCode(BuildContext context) { + String confirmSignInWithOtpCode(BuildContext context) { return AuthenticatorLocalizations.titlesOf(context) - .confirmSignInWithEmailMfaCode; + .confirmSignInWithOtpCode; } /// The title for the continue sign in (email MFA setup) Widget. @@ -99,8 +99,8 @@ class TitleResolver extends Resolver { return continueSignInWithTotpSetup(context); case AuthenticatorStep.confirmSignInWithTotpMfaCode: return confirmSignInWithTotpMfaCode(context); - case AuthenticatorStep.confirmSignInWithEmailMfaCode: - return confirmSignInWithEmailMfaCode(context); + case AuthenticatorStep.confirmSignInWithOtpCode: + return confirmSignInWithOtpCode(context); case AuthenticatorStep.continueSignInWithEmailMfaSetup: return continueSignInWithEmailMfaSetup(context); case AuthenticatorStep.continueSignInWithMfaSetupSelection: diff --git a/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart b/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart index e405a6a812..2524d2cb35 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart @@ -40,8 +40,8 @@ class AuthenticatorScreen extends StatelessAuthenticatorComponent { const AuthenticatorScreen.confirmSignInWithTotpMfaCode({Key? key}) : this(key: key, step: AuthenticatorStep.confirmSignInWithTotpMfaCode); - const AuthenticatorScreen.confirmSignInWithEmailMfaCode({Key? key}) - : this(key: key, step: AuthenticatorStep.confirmSignInWithEmailMfaCode); + const AuthenticatorScreen.confirmSignInWithOtpCode({Key? key}) + : this(key: key, step: AuthenticatorStep.confirmSignInWithOtpCode); const AuthenticatorScreen.continueSignInWithEmailMfaSetup({Key? key}) : this(key: key, step: AuthenticatorStep.continueSignInWithEmailMfaSetup); @@ -103,7 +103,7 @@ class AuthenticatorScreen extends StatelessAuthenticatorComponent { case AuthenticatorStep.confirmResetPassword: case AuthenticatorStep.verifyUser: case AuthenticatorStep.confirmVerifyUser: - case AuthenticatorStep.confirmSignInWithEmailMfaCode: + case AuthenticatorStep.confirmSignInWithOtpCode: case AuthenticatorStep.continueSignInWithEmailMfaSetup: case AuthenticatorStep.continueSignInWithMfaSetupSelection: child = _FormWrapperView(step: step); @@ -314,7 +314,7 @@ extension on AuthenticatorStep { case AuthenticatorStep.verifyUser: case AuthenticatorStep.confirmVerifyUser: case AuthenticatorStep.loading: - case AuthenticatorStep.confirmSignInWithEmailMfaCode: + case AuthenticatorStep.confirmSignInWithOtpCode: case AuthenticatorStep.continueSignInWithEmailMfaSetup: case AuthenticatorStep.continueSignInWithMfaSetupSelection: throw StateError('Invalid step: $this'); diff --git a/packages/authenticator/amplify_authenticator/lib/src/state/auth_state.dart b/packages/authenticator/amplify_authenticator/lib/src/state/auth_state.dart index 834afbe683..ed4d213700 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/state/auth_state.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/state/auth_state.dart @@ -49,8 +49,8 @@ class UnauthenticatedState extends AuthState static const continueSignInWithEmailMfaSetup = UnauthenticatedState( step: AuthenticatorStep.continueSignInWithEmailMfaSetup, ); - static const confirmSignInWithEmailMfaCode = UnauthenticatedState( - step: AuthenticatorStep.confirmSignInWithEmailMfaCode, + static const confirmSignInWithOtpCode = UnauthenticatedState( + step: AuthenticatorStep.confirmSignInWithOtpCode, ); static const resetPassword = UnauthenticatedState(step: AuthenticatorStep.resetPassword); diff --git a/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart b/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart index 2b75184038..12717df25b 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/state/inherited_forms.dart @@ -20,7 +20,7 @@ class InheritedForms extends InheritedWidget { required this.continueSignInWithTotpSetupForm, required this.continueSignInWithEmailMfaSetupForm, required this.confirmSignInWithTotpMfaCodeForm, - required this.confirmSignInWithEmailMfaCodeForm, + required this.confirmSignInWithOtpCodeForm, required this.verifyUserForm, required this.confirmVerifyUserForm, required super.child, @@ -38,7 +38,7 @@ class InheritedForms extends InheritedWidget { final ContinueSignInWithTotpSetupForm continueSignInWithTotpSetupForm; final ContinueSignInWithEmailMfaSetupForm continueSignInWithEmailMfaSetupForm; final ConfirmSignInMFAForm confirmSignInWithTotpMfaCodeForm; - final ConfirmSignInMFAForm confirmSignInWithEmailMfaCodeForm; + final ConfirmSignInMFAForm confirmSignInWithOtpCodeForm; final ResetPasswordForm resetPasswordForm; final ConfirmResetPasswordForm confirmResetPasswordForm; final VerifyUserForm verifyUserForm; @@ -70,8 +70,8 @@ class InheritedForms extends InheritedWidget { return confirmSignInWithTotpMfaCodeForm; case AuthenticatorStep.continueSignInWithEmailMfaSetup: return continueSignInWithEmailMfaSetupForm; - case AuthenticatorStep.confirmSignInWithEmailMfaCode: - return confirmSignInWithEmailMfaCodeForm; + case AuthenticatorStep.confirmSignInWithOtpCode: + return confirmSignInWithOtpCodeForm; case AuthenticatorStep.resetPassword: return resetPasswordForm; case AuthenticatorStep.confirmResetPassword: @@ -118,8 +118,8 @@ class InheritedForms extends InheritedWidget { continueSignInWithTotpSetupForm || oldWidget.confirmSignInWithTotpMfaCodeForm != confirmSignInWithTotpMfaCodeForm || - oldWidget.confirmSignInWithEmailMfaCodeForm != - confirmSignInWithEmailMfaCodeForm || + oldWidget.confirmSignInWithOtpCodeForm != + confirmSignInWithOtpCodeForm || oldWidget.continueSignInWithEmailMfaSetupForm != continueSignInWithEmailMfaSetupForm || oldWidget.continueSignInWithMfaSetupSelectionForm != diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart index b8a7bd15b1..abad5f4369 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form.dart @@ -679,7 +679,7 @@ class ContinueSignInWithEmailMfaSetupForm extends AuthenticatorForm { super.key, }) : super._( fields: [ - EmailSetupFormField.email(), + const EmailSetupFormField.email(), ], actions: const [ ContinueSignInWithEmailMfaSetupButton(), diff --git a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart index cc538d7dc8..83f862044e 100644 --- a/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart +++ b/packages/authenticator/amplify_authenticator/lib/src/widgets/form_field.dart @@ -231,7 +231,7 @@ abstract class AuthenticatorFormFieldState< state.confirmSignInCustomAuth(); case AuthenticatorStep.confirmSignInMfa: state.confirmSignInMFA(); - case AuthenticatorStep.confirmSignInWithEmailMfaCode: + case AuthenticatorStep.confirmSignInWithOtpCode: state.confirmEmailMfa(); case AuthenticatorStep.confirmSignInNewPassword: state.confirmSignInNewPassword(); diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_darkMode_desktopGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_darkMode_desktopGeometry.png new file mode 100644 index 0000000000..a6420c3365 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_darkMode_desktopGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_darkMode_mobileGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_darkMode_mobileGeometry.png new file mode 100644 index 0000000000..a846131d90 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_darkMode_mobileGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_lightMode_desktopGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_lightMode_desktopGeometry.png new file mode 100644 index 0000000000..26e399a433 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_lightMode_desktopGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_lightMode_mobileGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_lightMode_mobileGeometry.png new file mode 100644 index 0000000000..1f1bee56e3 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_confirmSignInWithOtpCodeStep_defaultMaterialTheme_lightMode_mobileGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_darkMode_desktopGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_darkMode_desktopGeometry.png new file mode 100644 index 0000000000..046d31c7c3 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_darkMode_desktopGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_darkMode_mobileGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_darkMode_mobileGeometry.png new file mode 100644 index 0000000000..565ef42835 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_darkMode_mobileGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_lightMode_desktopGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_lightMode_desktopGeometry.png new file mode 100644 index 0000000000..9b30f02df2 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_lightMode_desktopGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_lightMode_mobileGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_lightMode_mobileGeometry.png new file mode 100644 index 0000000000..c9fed4c010 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithEmailMfaSetupStep_defaultMaterialTheme_lightMode_mobileGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_darkMode_desktopGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_darkMode_desktopGeometry.png index 53c27c7301..8a75ce5fa1 100644 Binary files a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_darkMode_desktopGeometry.png and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_darkMode_desktopGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_darkMode_mobileGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_darkMode_mobileGeometry.png index 21795345c9..dd709da29a 100644 Binary files a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_darkMode_mobileGeometry.png and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_darkMode_mobileGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_lightMode_desktopGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_lightMode_desktopGeometry.png index 17569d4a11..6e1e3bda1d 100644 Binary files a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_lightMode_desktopGeometry.png and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_lightMode_desktopGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_lightMode_mobileGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_lightMode_mobileGeometry.png index 1a8803016a..26145cb92c 100644 Binary files a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_lightMode_mobileGeometry.png and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSelectionStep_defaultMaterialTheme_lightMode_mobileGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_darkMode_desktopGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_darkMode_desktopGeometry.png new file mode 100644 index 0000000000..c7f2fbe43b Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_darkMode_desktopGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_darkMode_mobileGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_darkMode_mobileGeometry.png new file mode 100644 index 0000000000..a25542f90f Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_darkMode_mobileGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_lightMode_desktopGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_lightMode_desktopGeometry.png new file mode 100644 index 0000000000..07ff1fc98c Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_lightMode_desktopGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_lightMode_mobileGeometry.png b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_lightMode_mobileGeometry.png new file mode 100644 index 0000000000..5f27b9f6a5 Binary files /dev/null and b/packages/authenticator/amplify_authenticator/test/ui/goldens/theme_emailConfig_continueSignInWithMfaSetupSelectionStep_defaultMaterialTheme_lightMode_mobileGeometry.png differ diff --git a/packages/authenticator/amplify_authenticator_test/lib/amplify_authenticator_test.dart b/packages/authenticator/amplify_authenticator_test/lib/amplify_authenticator_test.dart index d247d71507..49b9300e16 100644 --- a/packages/authenticator/amplify_authenticator_test/lib/amplify_authenticator_test.dart +++ b/packages/authenticator/amplify_authenticator_test/lib/amplify_authenticator_test.dart @@ -18,6 +18,7 @@ export 'src/pages/authenticator_page.dart'; export 'src/pages/confirm_sign_in_page.dart'; export 'src/pages/confirm_sign_up_page.dart'; export 'src/pages/confirm_verify_user_page.dart'; +export 'src/pages/email_mfa_setup_page.dart'; export 'src/pages/forgot_password_page.dart'; export 'src/pages/sign_in_page.dart'; export 'src/pages/sign_up_page.dart'; diff --git a/packages/authenticator/amplify_authenticator_test/lib/src/mock_authenticator_app.dart b/packages/authenticator/amplify_authenticator_test/lib/src/mock_authenticator_app.dart index 51f4a84830..af0fbc10f5 100644 --- a/packages/authenticator/amplify_authenticator_test/lib/src/mock_authenticator_app.dart +++ b/packages/authenticator/amplify_authenticator_test/lib/src/mock_authenticator_app.dart @@ -72,6 +72,7 @@ class _MockAuthenticatorAppState extends State { allowedMfaTypes: { MfaType.totp, MfaType.sms, + MfaType.email, }, ), ); @@ -87,6 +88,16 @@ class _MockAuthenticatorAppState extends State { ), ), ); + case AuthenticatorStep.continueSignInWithMfaSetupSelection: + baseBloc.setState( + const ContinueSignInWithMfaSetupSelection( + allowedMfaTypes: { + MfaType.sms, + MfaType.totp, + MfaType.email, + }, + ), + ); default: baseBloc.add(const AuthLoad()); break; diff --git a/packages/authenticator/amplify_authenticator_test/lib/src/pages/authenticator_page.dart b/packages/authenticator/amplify_authenticator_test/lib/src/pages/authenticator_page.dart index 4b4da3226b..78eb6148c6 100644 --- a/packages/authenticator/amplify_authenticator_test/lib/src/pages/authenticator_page.dart +++ b/packages/authenticator/amplify_authenticator_test/lib/src/pages/authenticator_page.dart @@ -42,6 +42,21 @@ abstract class AuthenticatorPage { expect(usernameFieldHint, isPresent ? findsOneWidget : findsNothing); } + /// Then I see "Email" as an input field + void expectEmail({ + String label = 'Email', + bool isPresent = true, + }) { + // email field is present + expect(usernameField, findsOneWidget); + // login type is "email" + final usernameFieldHint = find.descendant( + of: usernameField, + matching: find.text(label), + ); + expect(usernameFieldHint, isPresent ? findsOneWidget : findsNothing); + } + /// Expects the current step to be [step]. void expectStep(AuthenticatorStep step) { final currentScreen = tester.widget( diff --git a/packages/authenticator/amplify_authenticator_test/lib/src/pages/confirm_sign_in_page.dart b/packages/authenticator/amplify_authenticator_test/lib/src/pages/confirm_sign_in_page.dart index 84c8f8cbfb..5d2d352509 100644 --- a/packages/authenticator/amplify_authenticator_test/lib/src/pages/confirm_sign_in_page.dart +++ b/packages/authenticator/amplify_authenticator_test/lib/src/pages/confirm_sign_in_page.dart @@ -25,8 +25,12 @@ class ConfirmSignInPage extends AuthenticatorPage { Finder get confirmSignInButton => find.byKey(keyConfirmSignInButton); Finder get confirmSignInMfaSelectionButton => find.byKey(keyConfirmSignInMfaSelectionButton); + Finder get confirmSignInMfaSetupSelectionButton => + find.byKey(keyConfirmSignInMfaSetupSelectionButton); Finder get selectMfaRadio => find.byKey(keyMfaMethodRadioConfirmSignInFormField); + Finder get selectMfaSetupRadio => + find.byKey(keyMfaSetupMethodRadioConfirmSignInFormField); Finder get backToSignIn => find.byKey(keyBackToSignInButton); /// Then I see "Confirm Sign In - New Password" @@ -59,6 +63,28 @@ class ConfirmSignInPage extends AuthenticatorPage { ); } + /// Then I see "Select an MFA Method to set up" + Future expectContinueSignInWithMfaSetupSelectionIsPresent() async { + final currentScreen = tester.widget( + find.byType(AuthenticatorScreen), + ); + expect( + currentScreen.step, + equals(AuthenticatorStep.continueSignInWithMfaSetupSelection), + ); + } + + /// Then I see "Enter your one-time passcode for Email" + Future expectConfirmSignInWithOtpCodeIsPresent() async { + final currentScreen = tester.widget( + find.byType(AuthenticatorScreen), + ); + expect( + currentScreen.step, + equals(AuthenticatorStep.confirmSignInWithOtpCode), + ); + } + /// Then I see "Setup an Authentication App" Future expectSignInTotpSetupIsPresent() async { final currentScreen = tester.widget( @@ -86,6 +112,12 @@ class ConfirmSignInPage extends AuthenticatorPage { expect(newPasswordField, findsOneWidget); } + /// Then I see "Invalid verification code" + @override + void expectInvalidVerificationCode() { + expectError('Invalid code'); + } + /// When I enter a verification code Future enterVerificationCode(String code) async { await tester.ensureVisible(verificationField); @@ -123,9 +155,34 @@ class ConfirmSignInPage extends AuthenticatorPage { }) async { expect(selectMfaRadio, findsOneWidget); + // if mfaMethod is email, don't make it uppercase except for the first letter + // if mfa method is totp, make it all uppercase final mfaMethodWidget = find.descendant( of: selectMfaRadio, - matching: find.textContaining('(${mfaMethod.name.toUpperCase()})'), + matching: find.textContaining( + mfaMethod == MfaType.email + ? 'Email' + : '(${mfaMethod.name.toUpperCase()})', + ), + ); + + await tester.tap(mfaMethodWidget); + await tester.pumpAndSettle(); + } + + // When I select a MFA setup method + Future selectMfaSetupMethod({ + required MfaType mfaMethod, + }) async { + expect(selectMfaSetupRadio, findsOneWidget); + + final mfaMethodWidget = find.descendant( + of: selectMfaSetupRadio, + matching: find.textContaining( + mfaMethod == MfaType.email + ? 'Email' + : '(${mfaMethod.name.toUpperCase()})', + ), ); await tester.tap(mfaMethodWidget); @@ -139,6 +196,13 @@ class ConfirmSignInPage extends AuthenticatorPage { await tester.pumpAndSettle(); } + /// When I click the continue sign in with MFA setup selection button + Future submitConfirmSignInMfaSetupSelection() async { + await tester.ensureVisible(confirmSignInMfaSetupSelectionButton); + await tester.tap(confirmSignInMfaSetupSelectionButton); + await tester.pumpAndSettle(); + } + /// When I navigate to the "Sign In" step. Future navigateToSignIn() async { await tester.tap(backToSignIn); diff --git a/packages/authenticator/amplify_authenticator_test/lib/src/pages/email_mfa_setup_page.dart b/packages/authenticator/amplify_authenticator_test/lib/src/pages/email_mfa_setup_page.dart new file mode 100644 index 0000000000..546049a4b6 --- /dev/null +++ b/packages/authenticator/amplify_authenticator_test/lib/src/pages/email_mfa_setup_page.dart @@ -0,0 +1,46 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_authenticator/amplify_authenticator.dart'; +// ignore: implementation_imports +import 'package:amplify_authenticator/src/keys.dart'; +// ignore: implementation_imports +import 'package:amplify_authenticator/src/screens/authenticator_screen.dart'; +import 'package:amplify_authenticator_test/src/pages/authenticator_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class EmailMfaSetupPage extends AuthenticatorPage { + EmailMfaSetupPage({required super.tester}); + + @override + Finder get usernameField => throw UnimplementedError(); + + Finder get emailField => find.byKey(keyEmailSetupFormField); + Finder get continueSignIn => + find.byKey(keyConfirmSignInWithEmailMfaSetupButton); + + /// When I type my email + Future enterEmail(String email) async { + await tester.ensureVisible(emailField); + await tester.enterText(emailField, email); + await tester.pumpAndSettle(); + } + + /// Then I see "Add Email for Two-Factor Authentication" + Future expectEmailMfaSetupIsPresent() async { + final currentScreen = tester.widget( + find.byType(AuthenticatorScreen), + ); + expect( + currentScreen.step, + equals(AuthenticatorStep.continueSignInWithEmailMfaSetup), + ); + } + + /// When I enter an email + Future submitEmail() async { + await tester.ensureVisible(continueSignIn); + await tester.tap(continueSignIn); + await tester.pumpAndSettle(); + } +} diff --git a/packages/test/amplify_auth_integration_test/lib/amplify_auth_integration_test.dart b/packages/test/amplify_auth_integration_test/lib/amplify_auth_integration_test.dart index 0e94ac7fd3..2e3d46e086 100644 --- a/packages/test/amplify_auth_integration_test/lib/amplify_auth_integration_test.dart +++ b/packages/test/amplify_auth_integration_test/lib/amplify_auth_integration_test.dart @@ -6,6 +6,7 @@ library amplify_auth_integration_test; export 'src/async_test.dart'; +export 'src/email_utils.dart'; export 'src/environments.dart'; export 'src/test_auth_plugin.dart'; export 'src/test_runner.dart'; diff --git a/packages/test/amplify_auth_integration_test/lib/src/email_utils.dart b/packages/test/amplify_auth_integration_test/lib/src/email_utils.dart new file mode 100644 index 0000000000..ffb0534cd0 --- /dev/null +++ b/packages/test/amplify_auth_integration_test/lib/src/email_utils.dart @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Sets up EMAIL MFA for the current user. +Future setUpEmailMfa() async { + final plugin = Amplify.Auth.getPlugin(AmplifyAuthCognito.pluginKey); + await plugin.updateMfaPreference(email: MfaPreference.preferred); +} diff --git a/packages/test/amplify_auth_integration_test/lib/src/environments.dart b/packages/test/amplify_auth_integration_test/lib/src/environments.dart index f6b9ebee6f..1b2ab30f67 100644 --- a/packages/test/amplify_auth_integration_test/lib/src/environments.dart +++ b/packages/test/amplify_auth_integration_test/lib/src/environments.dart @@ -17,6 +17,12 @@ const List userPoolEnvironments = [ confirmationDeliveryMedium: DeliveryMedium.sms, resetPasswordDeliveryMedium: DeliveryMedium.sms, ), + EnvironmentInfo.withGen2Defaults( + name: 'email-sign-in', + loginMethod: LoginMethod.email, + confirmationDeliveryMedium: DeliveryMedium.email, + resetPasswordDeliveryMedium: DeliveryMedium.email, + ), ]; /// An environment with optional MFA via SMS only. @@ -55,6 +61,55 @@ const mfaRequiredSmsTotp = EnvironmentInfo.withGen1Defaults( mfaInfo: MfaInfo(smsEnabled: true, totpEnabled: true, required: true), ); +/// An environment with required MFA via Email only. +const mfaRequiredEmail = EnvironmentInfo.withGen2Defaults( + name: 'mfa-required-email', + mfaInfo: MfaInfo(emailEnabled: true, required: true), + loginMethod: LoginMethod.email, +); + +/// An environment with optional MFA via Email only. +const mfaOptionalEmail = EnvironmentInfo.withGen2Defaults( + name: 'mfa-optional-email', + mfaInfo: MfaInfo(emailEnabled: true, required: false), + loginMethod: LoginMethod.email, +); + +/// An environment with required MFA via Email & SMS. +const mfaRequiredEmailSms = EnvironmentInfo.withGen2Defaults( + name: 'mfa-required-email-sms', + mfaInfo: MfaInfo(emailEnabled: true, smsEnabled: true, required: true), + loginMethod: LoginMethod.email, +); + +/// An environment with optional MFA via Email & SMS. +const mfaOptionalEmailSms = EnvironmentInfo.withGen2Defaults( + name: 'mfa-optional-email-sms', + mfaInfo: MfaInfo(emailEnabled: true, smsEnabled: true, required: false), + loginMethod: LoginMethod.email, +); + +/// An environment with required MFA via Email & TOTP. +const mfaRequiredEmailTotp = EnvironmentInfo.withGen2Defaults( + name: 'mfa-required-email-totp', + mfaInfo: MfaInfo(emailEnabled: true, totpEnabled: true, required: true), + loginMethod: LoginMethod.email, +); + +/// An environment with optional MFA via Email & TOTP. +const mfaOptionalEmailTotp = EnvironmentInfo.withGen2Defaults( + name: 'mfa-optional-email-totp', + mfaInfo: MfaInfo(emailEnabled: true, totpEnabled: true, required: false), + loginMethod: LoginMethod.email, +); + +/// An environment with required MFA and username login. +const mfaRequiredUsernameLogin = EnvironmentInfo.withGen2Defaults( + name: 'username-login-mfa', + mfaInfo: MfaInfo(totpEnabled: true, emailEnabled: true, required: true), + loginMethod: LoginMethod.username, +); + /// Environments that support MFA const List mfaEnvironments = [ mfaOptionalSms, @@ -63,6 +118,13 @@ const List mfaEnvironments = [ mfaRequiredTotp, mfaOptionalSmsTotp, mfaRequiredSmsTotp, + mfaRequiredEmail, + mfaOptionalEmail, + mfaRequiredEmailSms, + mfaOptionalEmailSms, + mfaRequiredEmailTotp, + mfaOptionalEmailTotp, + mfaRequiredUsernameLogin, ]; /// Environments with a user pool and opt-in device tracking.