Skip to content

Commit 0500755

Browse files
feat: add email MFA steps to Authenticator (#5389)
1 parent 4a5d9ed commit 0500755

22 files changed

+462
-38
lines changed

infra-gen2/tool/deploy_gen2.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ import 'package:path/path.dart' as p;
1919
/// 3. Add the backend to a category or create a new category
2020
/// 4. Run `dart tool/deploy_gen2.dart` to deploy the backend
2121
const List<AmplifyBackendGroup> infraConfig = [
22-
AmplifyBackendGroup(
23-
category: Category.analytics,
24-
defaultOutput: '',
25-
backends: [],
26-
),
2722
AmplifyBackendGroup(
2823
category: Category.api,
2924
defaultOutput: 'packages/api/amplify_api/example/lib',

packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -481,45 +481,45 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
481481
}
482482

483483
CognitoSignInResult _processSignInResult(SignInState result) {
484-
return switch (result) {
485-
SignInNotStarted _ ||
486-
SignInInitiating _ =>
484+
switch (result) {
485+
case SignInNotStarted():
486+
case SignInInitiating():
487487
// This should never happen.
488488
throw UnknownException(
489489
'Sign in could not be completed',
490490
underlyingException: result,
491-
),
492-
SignInCancelling _ => throw const UserCancelledException(
491+
);
492+
493+
case SignInCancelling():
494+
throw const UserCancelledException(
493495
'The user canceled the sign-in flow',
494-
),
495-
SignInChallenge(
496-
:final challengeName,
497-
:final challengeParameters,
498-
:final codeDeliveryDetails,
499-
:final requiredAttributes,
500-
:final allowedMfaTypes,
501-
:final totpSetupResult,
502-
) =>
503-
CognitoSignInResult(
496+
);
497+
498+
case final SignInChallenge challenge:
499+
return CognitoSignInResult(
504500
isSignedIn: false,
505501
nextStep: AuthNextSignInStep(
506-
signInStep: challengeName.signInStep,
507-
codeDeliveryDetails: codeDeliveryDetails,
508-
additionalInfo: challengeParameters,
509-
missingAttributes: requiredAttributes,
510-
allowedMfaTypes: allowedMfaTypes,
511-
totpSetupDetails: totpSetupResult,
502+
signInStep: challenge.challengeName.signInStep,
503+
codeDeliveryDetails: challenge.codeDeliveryDetails,
504+
additionalInfo: challenge.challengeParameters,
505+
missingAttributes: challenge.requiredAttributes,
506+
allowedMfaTypes: challenge.allowedMfaTypes,
507+
totpSetupDetails: challenge.totpSetupResult,
512508
),
513-
),
514-
SignInSuccess _ => const CognitoSignInResult(
509+
);
510+
511+
case SignInSuccess():
512+
return const CognitoSignInResult(
515513
isSignedIn: true,
516514
nextStep: AuthNextSignInStep(
517515
signInStep: AuthSignInStep.done,
518516
),
519-
),
520-
SignInFailure(:final exception, :final stackTrace) =>
521-
Error.throwWithStackTrace(exception, stackTrace),
522-
};
517+
);
518+
519+
case final SignInFailure failure:
520+
Error.throwWithStackTrace(failure.exception, failure.stackTrace);
521+
// To satisfy Dart's requirements, even if unreachable
522+
}
523523
}
524524

525525
@override

packages/authenticator/amplify_authenticator/lib/amplify_authenticator.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ export 'src/widgets/form.dart'
7676
ConfirmSignInMFAForm,
7777
ConfirmSignInNewPasswordForm,
7878
ContinueSignInWithMfaSelectionForm,
79+
ContinueSignInWithMfaSetupSelectionForm,
7980
ContinueSignInWithTotpSetupForm,
81+
ContinueSignInWithEmailMfaSetupForm,
8082
ConfirmSignUpForm,
8183
ResetPasswordForm,
8284
ConfirmResetPasswordForm,
@@ -710,9 +712,14 @@ class _AuthenticatorState extends State<Authenticator> {
710712
confirmSignInMFAForm: ConfirmSignInMFAForm(),
711713
continueSignInWithMfaSelectionForm:
712714
ContinueSignInWithMfaSelectionForm(),
715+
continueSignInWithMfaSetupSelectionForm:
716+
ContinueSignInWithMfaSetupSelectionForm(),
713717
continueSignInWithTotpSetupForm:
714718
ContinueSignInWithTotpSetupForm(),
719+
continueSignInWithEmailMfaSetupForm:
720+
ContinueSignInWithEmailMfaSetupForm(),
715721
confirmSignInWithTotpMfaCodeForm: ConfirmSignInMFAForm(),
722+
confirmSignInWithEmailMfaCodeForm: ConfirmSignInMFAForm(),
716723
verifyUserForm: VerifyUserForm(),
717724
confirmVerifyUserForm: ConfirmVerifyUserForm(),
718725
child: widget.child,

packages/authenticator/amplify_authenticator/lib/src/blocs/auth/auth_bloc.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ class StateMachineBloc
340340
),
341341
);
342342
case AuthSignInStep.continueSignInWithMfaSetupSelection:
343-
await _handleMfaSetupSelection(result);
343+
_emit(await _handleMfaSetupSelection(result));
344344
case AuthSignInStep.continueSignInWithEmailMfaSetup:
345345
_emit(UnauthenticatedState.continueSignInWithEmailMfaSetup);
346346
case AuthSignInStep.confirmSignInWithTotpMfaCode:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
enum EmailSetupField {
5+
email,
6+
}

packages/authenticator/amplify_authenticator/lib/src/enums/enums.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export 'authenticator_step.dart';
55
export 'confirm_signin_types.dart';
66
export 'confirm_signup_types.dart';
7+
export 'email_setup_types.dart';
78
export 'gender.dart';
89
export 'reset_password_field.dart';
910
export 'signin_types.dart';

packages/authenticator/amplify_authenticator/lib/src/keys.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ const keyCustomChallengeConfirmSignInFormField =
5252
Key('customChallengeConfirmSignInFormField');
5353
const keyMfaMethodRadioConfirmSignInFormField =
5454
Key('mfaMethodRadioConfirmSignInFormField');
55+
const keyMfaSetupMethodRadioConfirmSignInFormField =
56+
Key('mfaSetupMethodRadioConfirmSignInFormField');
5557
const keyUsernameConfirmSignInFormField = Key('usernameConfirmSignInFormField');
5658
const keyPasswordConfirmSignInFormField = Key('passwordConfirmSignInFormField');
5759
const keyNewPasswordConfirmSignInFormField =
@@ -107,6 +109,10 @@ const keyGoToSignInButton = Key('goToSignInButton');
107109
const keyConfirmSignInButton = Key('confirmSignInButton');
108110
const keyConfirmSignInMfaSelectionButton =
109111
Key('confirmSignInMfaSelectionButton');
112+
const keyConfirmSignInMfaSetupSelectionButton =
113+
Key('confirmSignInMfaSetupSelectionButton');
114+
const keyConfirmSignInWithEmailMfaSetupButton =
115+
Key('confirmSignInWithEmailMfaSetupButton');
110116
const keyConfirmSignInCustomButton = Key('confirmSignInCustomButton');
111117
const keyLostCodeButton = Key('lostCodeButton');
112118
const keySendCodeButton = Key('sendCodeButton');
@@ -140,3 +146,6 @@ const keyAuthenticatorBanner = Key('authenticatorBanner');
140146
const keyQrCodeTotpSetupFormField = Key('qrCodeTotpSetupFormField');
141147
const keyCopyKeyTotpSetupFormField = Key('copyKeyTotpSetupFormField');
142148
const keyTotpSetupFormField = Key('totpSetupFormField');
149+
150+
// Email setup form keys
151+
const keyEmailSetupFormField = Key('emailSetupFormField');

packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,24 @@ abstract class AuthenticatorTitleLocalizations {
141141
/// **'Enter your one-time passcode'**
142142
String get confirmSignInWithTotpMfaCode;
143143

144+
/// Title of the Confirm Sign In with Email MFA Code step and form
145+
///
146+
/// In en, this message translates to:
147+
/// **'Enter your one-time passcode'**
148+
String get confirmSignInWithEmailMfaCode;
149+
150+
/// Title of the Continue Sign In with Email MFA Setup step and form
151+
///
152+
/// In en, this message translates to:
153+
/// **'Add Email for Two-Factor Authentication'**
154+
String get continueSignInWithEmailMfaSetup;
155+
156+
/// Title of the Continue Sign In with MFA Setup Selection step and form
157+
///
158+
/// In en, this message translates to:
159+
/// **'Choose your preferred two-factor authentication method to set up'**
160+
String get continueSignInWithMfaSetupSelection;
161+
144162
/// Title of the Reset Password step and form
145163
///
146164
/// In en, this message translates to:

packages/authenticator/amplify_authenticator/lib/src/l10n/generated/title_localizations_en.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ class AuthenticatorTitleLocalizationsEn
3030
@override
3131
String get confirmSignInWithTotpMfaCode => 'Enter your one-time passcode';
3232

33+
@override
34+
String get confirmSignInWithEmailMfaCode => 'Enter your one-time passcode';
35+
36+
@override
37+
String get continueSignInWithEmailMfaSetup =>
38+
'Add Email for Two-Factor Authentication';
39+
40+
@override
41+
String get continueSignInWithMfaSetupSelection =>
42+
'Choose your preferred two-factor authentication method to set up';
43+
3344
@override
3445
String get resetPassword => 'Send Code';
3546

packages/authenticator/amplify_authenticator/lib/src/l10n/input_resolver.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class InputResolverKey {
174174

175175
static const selectEmail = InputResolverKey._(
176176
InputResolverKeyType.title,
177-
field: InputField.email,
177+
field: InputField.selectEmail,
178178
);
179179

180180
static const totpCodePrompt = InputResolverKey._(

packages/authenticator/amplify_authenticator/lib/src/l10n/src/inputs/inputs_en.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,5 +216,9 @@
216216
"totpCodePrompt": "Please enter the code from your registered Authenticator app",
217217
"@totpCodePrompt": {
218218
"description": "The instructional text for submitting a TOTP pass code"
219-
}
219+
},
220+
"selectEmail": "Email",
221+
"@selectEmail": {
222+
"description": "Label for the radio button to select email as the user's chosen MFA method."
223+
},
220224
}

packages/authenticator/amplify_authenticator/lib/src/l10n/title_resolver.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ class TitleResolver extends Resolver<AuthenticatorStep> {
4949
.confirmSignInWithTotpMfaCode;
5050
}
5151

52+
/// The title for the confirm sign in (email MFA code) Widget.
53+
String confirmSignInWithEmailMfaCode(BuildContext context) {
54+
return AuthenticatorLocalizations.titlesOf(context)
55+
.confirmSignInWithEmailMfaCode;
56+
}
57+
58+
/// The title for the continue sign in (email MFA setup) Widget.
59+
String continueSignInWithEmailMfaSetup(BuildContext context) {
60+
return AuthenticatorLocalizations.titlesOf(context)
61+
.continueSignInWithEmailMfaSetup;
62+
}
63+
64+
/// The title for the continue sign in (mfa setup selection) Widget.
65+
String continueSignInWithMfaSetupSelection(BuildContext context) {
66+
return AuthenticatorLocalizations.titlesOf(context)
67+
.continueSignInWithMfaSetupSelection;
68+
}
69+
5270
/// The title for the reset password Widget.
5371
String resetPassword(BuildContext context) {
5472
return AuthenticatorLocalizations.titlesOf(context).resetPassword;
@@ -81,6 +99,12 @@ class TitleResolver extends Resolver<AuthenticatorStep> {
8199
return continueSignInWithTotpSetup(context);
82100
case AuthenticatorStep.confirmSignInWithTotpMfaCode:
83101
return confirmSignInWithTotpMfaCode(context);
102+
case AuthenticatorStep.confirmSignInWithEmailMfaCode:
103+
return confirmSignInWithEmailMfaCode(context);
104+
case AuthenticatorStep.continueSignInWithEmailMfaSetup:
105+
return continueSignInWithEmailMfaSetup(context);
106+
case AuthenticatorStep.continueSignInWithMfaSetupSelection:
107+
return continueSignInWithMfaSetupSelection(context);
84108
case AuthenticatorStep.resetPassword:
85109
return resetPassword(context);
86110
case AuthenticatorStep.confirmResetPassword:

packages/authenticator/amplify_authenticator/lib/src/screens/authenticator_screen.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ class AuthenticatorScreen extends StatelessAuthenticatorComponent {
4040
const AuthenticatorScreen.confirmSignInWithTotpMfaCode({Key? key})
4141
: this(key: key, step: AuthenticatorStep.confirmSignInWithTotpMfaCode);
4242

43+
const AuthenticatorScreen.confirmSignInWithEmailMfaCode({Key? key})
44+
: this(key: key, step: AuthenticatorStep.confirmSignInWithEmailMfaCode);
45+
46+
const AuthenticatorScreen.continueSignInWithEmailMfaSetup({Key? key})
47+
: this(key: key, step: AuthenticatorStep.continueSignInWithEmailMfaSetup);
48+
49+
const AuthenticatorScreen.continueSignInWithMfaSetupSelection({Key? key})
50+
: this(
51+
key: key,
52+
step: AuthenticatorStep.continueSignInWithMfaSetupSelection,
53+
);
54+
4355
const AuthenticatorScreen.resetPassword({Key? key})
4456
: this(key: key, step: AuthenticatorStep.resetPassword);
4557

@@ -91,6 +103,9 @@ class AuthenticatorScreen extends StatelessAuthenticatorComponent {
91103
case AuthenticatorStep.confirmResetPassword:
92104
case AuthenticatorStep.verifyUser:
93105
case AuthenticatorStep.confirmVerifyUser:
106+
case AuthenticatorStep.confirmSignInWithEmailMfaCode:
107+
case AuthenticatorStep.continueSignInWithEmailMfaSetup:
108+
case AuthenticatorStep.continueSignInWithMfaSetupSelection:
94109
child = _FormWrapperView(step: step);
95110
case AuthenticatorStep.loading:
96111
throw StateError('Invalid step: $this');
@@ -299,6 +314,9 @@ extension on AuthenticatorStep {
299314
case AuthenticatorStep.verifyUser:
300315
case AuthenticatorStep.confirmVerifyUser:
301316
case AuthenticatorStep.loading:
317+
case AuthenticatorStep.confirmSignInWithEmailMfaCode:
318+
case AuthenticatorStep.continueSignInWithEmailMfaSetup:
319+
case AuthenticatorStep.continueSignInWithMfaSetupSelection:
302320
throw StateError('Invalid step: $this');
303321
}
304322
}

packages/authenticator/amplify_authenticator/lib/src/state/authenticator_state.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,18 @@ class AuthenticatorState extends ChangeNotifier {
160160
notifyListeners();
161161
}
162162

163+
/// The value for the email MFA setup form field
164+
///
165+
/// This value will be used during continue email MFA setup
166+
String get mfaEmail => _mfaEmail;
167+
168+
set mfaEmail(String value) {
169+
_mfaEmail = value.trim();
170+
notifyListeners();
171+
}
172+
173+
String _mfaEmail = '';
174+
163175
MfaType? _selectedMfaMethod;
164176

165177
TotpSetupDetails? get totpSetupDetails {
@@ -395,6 +407,23 @@ class AuthenticatorState extends ChangeNotifier {
395407
_setIsBusy(false);
396408
}
397409

410+
/// Select MFA setup preference using the values for [selectedMfaMethod]
411+
Future<void> continueSignInWithMfaSetupSelection() async {
412+
if (!_formKey.currentState!.validate()) {
413+
return;
414+
}
415+
416+
_setIsBusy(true);
417+
418+
final confirm = AuthConfirmSignInData(
419+
confirmationValue: _selectedMfaMethod!.name,
420+
);
421+
422+
_authBloc.add(AuthConfirmSignIn(confirm));
423+
await nextBlocEvent();
424+
_setIsBusy(false);
425+
}
426+
398427
/// Complete TOTP setup using the values for [confirmationCode]
399428
/// and any user attributes.
400429
Future<void> confirmTotp() async {
@@ -414,6 +443,40 @@ class AuthenticatorState extends ChangeNotifier {
414443
_setIsBusy(false);
415444
}
416445

446+
/// complete Email MFA setup using the values for [confirmationCode]
447+
Future<void> confirmEmailMfa() async {
448+
if (!_formKey.currentState!.validate()) {
449+
return;
450+
}
451+
452+
_setIsBusy(true);
453+
454+
final confirm = AuthConfirmSignInData(
455+
confirmationValue: _confirmationCode.trim(),
456+
);
457+
458+
_authBloc.add(AuthConfirmSignIn(confirm));
459+
await nextBlocEvent();
460+
_setIsBusy(false);
461+
}
462+
463+
/// Complete MFA setup using the values for [confirmationCode]
464+
Future<void> continueEmailMfaSetup() async {
465+
if (!_formKey.currentState!.validate()) {
466+
return;
467+
}
468+
469+
_setIsBusy(true);
470+
471+
final confirm = AuthConfirmSignInData(
472+
confirmationValue: _mfaEmail.trim(),
473+
);
474+
475+
_authBloc.add(AuthConfirmSignIn(confirm));
476+
await nextBlocEvent();
477+
_setIsBusy(false);
478+
}
479+
417480
/// Complete the force password change with [newPassword]
418481
Future<void> confirmSignInNewPassword() async {
419482
if (!_formKey.currentState!.validate()) {
@@ -644,6 +707,7 @@ class AuthenticatorState extends ChangeNotifier {
644707
authAttributes.clear();
645708
_publicChallengeParams.clear();
646709
_selectedMfaMethod = null;
710+
_mfaEmail = '';
647711
}
648712

649713
void _resetFormKey() {

0 commit comments

Comments
 (0)