Skip to content

Commit

Permalink
feat: add email MFA steps to Authenticator (#5389)
Browse files Browse the repository at this point in the history
  • Loading branch information
khatruong2009 authored Oct 22, 2024
1 parent 4a5d9ed commit 0500755
Show file tree
Hide file tree
Showing 22 changed files with 462 additions and 38 deletions.
5 changes: 0 additions & 5 deletions infra-gen2/tool/deploy_gen2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ import 'package:path/path.dart' as p;
/// 3. Add the backend to a category or create a new category
/// 4. Run `dart tool/deploy_gen2.dart` to deploy the backend
const List<AmplifyBackendGroup> infraConfig = [
AmplifyBackendGroup(
category: Category.analytics,
defaultOutput: '',
backends: [],
),
AmplifyBackendGroup(
category: Category.api,
defaultOutput: 'packages/api/amplify_api/example/lib',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,45 +481,45 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
}

CognitoSignInResult _processSignInResult(SignInState result) {
return switch (result) {
SignInNotStarted _ ||
SignInInitiating _ =>
switch (result) {
case SignInNotStarted():
case SignInInitiating():
// This should never happen.
throw UnknownException(
'Sign in could not be completed',
underlyingException: result,
),
SignInCancelling _ => throw const UserCancelledException(
);

case SignInCancelling():
throw const UserCancelledException(
'The user canceled the sign-in flow',
),
SignInChallenge(
:final challengeName,
:final challengeParameters,
:final codeDeliveryDetails,
:final requiredAttributes,
:final allowedMfaTypes,
:final totpSetupResult,
) =>
CognitoSignInResult(
);

case final SignInChallenge challenge:
return CognitoSignInResult(
isSignedIn: false,
nextStep: AuthNextSignInStep(
signInStep: challengeName.signInStep,
codeDeliveryDetails: codeDeliveryDetails,
additionalInfo: challengeParameters,
missingAttributes: requiredAttributes,
allowedMfaTypes: allowedMfaTypes,
totpSetupDetails: totpSetupResult,
signInStep: challenge.challengeName.signInStep,
codeDeliveryDetails: challenge.codeDeliveryDetails,
additionalInfo: challenge.challengeParameters,
missingAttributes: challenge.requiredAttributes,
allowedMfaTypes: challenge.allowedMfaTypes,
totpSetupDetails: challenge.totpSetupResult,
),
),
SignInSuccess _ => const CognitoSignInResult(
);

case SignInSuccess():
return const CognitoSignInResult(
isSignedIn: true,
nextStep: AuthNextSignInStep(
signInStep: AuthSignInStep.done,
),
),
SignInFailure(:final exception, :final stackTrace) =>
Error.throwWithStackTrace(exception, stackTrace),
};
);

case final SignInFailure failure:
Error.throwWithStackTrace(failure.exception, failure.stackTrace);
// To satisfy Dart's requirements, even if unreachable
}
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export 'src/widgets/form.dart'
ConfirmSignInMFAForm,
ConfirmSignInNewPasswordForm,
ContinueSignInWithMfaSelectionForm,
ContinueSignInWithMfaSetupSelectionForm,
ContinueSignInWithTotpSetupForm,
ContinueSignInWithEmailMfaSetupForm,
ConfirmSignUpForm,
ResetPasswordForm,
ConfirmResetPasswordForm,
Expand Down Expand Up @@ -710,9 +712,14 @@ class _AuthenticatorState extends State<Authenticator> {
confirmSignInMFAForm: ConfirmSignInMFAForm(),
continueSignInWithMfaSelectionForm:
ContinueSignInWithMfaSelectionForm(),
continueSignInWithMfaSetupSelectionForm:
ContinueSignInWithMfaSetupSelectionForm(),
continueSignInWithTotpSetupForm:
ContinueSignInWithTotpSetupForm(),
continueSignInWithEmailMfaSetupForm:
ContinueSignInWithEmailMfaSetupForm(),
confirmSignInWithTotpMfaCodeForm: ConfirmSignInMFAForm(),
confirmSignInWithEmailMfaCodeForm: ConfirmSignInMFAForm(),
verifyUserForm: VerifyUserForm(),
confirmVerifyUserForm: ConfirmVerifyUserForm(),
child: widget.child,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ class StateMachineBloc
),
);
case AuthSignInStep.continueSignInWithMfaSetupSelection:
await _handleMfaSetupSelection(result);
_emit(await _handleMfaSetupSelection(result));
case AuthSignInStep.continueSignInWithEmailMfaSetup:
_emit(UnauthenticatedState.continueSignInWithEmailMfaSetup);
case AuthSignInStep.confirmSignInWithTotpMfaCode:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

enum EmailSetupField {
email,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export 'authenticator_step.dart';
export 'confirm_signin_types.dart';
export 'confirm_signup_types.dart';
export 'email_setup_types.dart';
export 'gender.dart';
export 'reset_password_field.dart';
export 'signin_types.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const keyCustomChallengeConfirmSignInFormField =
Key('customChallengeConfirmSignInFormField');
const keyMfaMethodRadioConfirmSignInFormField =
Key('mfaMethodRadioConfirmSignInFormField');
const keyMfaSetupMethodRadioConfirmSignInFormField =
Key('mfaSetupMethodRadioConfirmSignInFormField');
const keyUsernameConfirmSignInFormField = Key('usernameConfirmSignInFormField');
const keyPasswordConfirmSignInFormField = Key('passwordConfirmSignInFormField');
const keyNewPasswordConfirmSignInFormField =
Expand Down Expand Up @@ -107,6 +109,10 @@ const keyGoToSignInButton = Key('goToSignInButton');
const keyConfirmSignInButton = Key('confirmSignInButton');
const keyConfirmSignInMfaSelectionButton =
Key('confirmSignInMfaSelectionButton');
const keyConfirmSignInMfaSetupSelectionButton =
Key('confirmSignInMfaSetupSelectionButton');
const keyConfirmSignInWithEmailMfaSetupButton =
Key('confirmSignInWithEmailMfaSetupButton');
const keyConfirmSignInCustomButton = Key('confirmSignInCustomButton');
const keyLostCodeButton = Key('lostCodeButton');
const keySendCodeButton = Key('sendCodeButton');
Expand Down Expand Up @@ -140,3 +146,6 @@ const keyAuthenticatorBanner = Key('authenticatorBanner');
const keyQrCodeTotpSetupFormField = Key('qrCodeTotpSetupFormField');
const keyCopyKeyTotpSetupFormField = Key('copyKeyTotpSetupFormField');
const keyTotpSetupFormField = Key('totpSetupFormField');

// Email setup form keys
const keyEmailSetupFormField = Key('emailSetupFormField');
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,24 @@ abstract class AuthenticatorTitleLocalizations {
/// **'Enter your one-time passcode'**
String get confirmSignInWithTotpMfaCode;

/// Title of the Confirm Sign In with Email MFA Code step and form
///
/// In en, this message translates to:
/// **'Enter your one-time passcode'**
String get confirmSignInWithEmailMfaCode;

/// Title of the Continue Sign In with Email MFA Setup step and form
///
/// In en, this message translates to:
/// **'Add Email for Two-Factor Authentication'**
String get continueSignInWithEmailMfaSetup;

/// Title of the Continue Sign In with MFA Setup Selection step and form
///
/// In en, this message translates to:
/// **'Choose your preferred two-factor authentication method to set up'**
String get continueSignInWithMfaSetupSelection;

/// Title of the Reset Password step and form
///
/// In en, this message translates to:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ class AuthenticatorTitleLocalizationsEn
@override
String get confirmSignInWithTotpMfaCode => 'Enter your one-time passcode';

@override
String get confirmSignInWithEmailMfaCode => 'Enter your one-time passcode';

@override
String get continueSignInWithEmailMfaSetup =>
'Add Email for Two-Factor Authentication';

@override
String get continueSignInWithMfaSetupSelection =>
'Choose your preferred two-factor authentication method to set up';

@override
String get resetPassword => 'Send Code';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class InputResolverKey {

static const selectEmail = InputResolverKey._(
InputResolverKeyType.title,
field: InputField.email,
field: InputField.selectEmail,
);

static const totpCodePrompt = InputResolverKey._(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,5 +216,9 @@
"totpCodePrompt": "Please enter the code from your registered Authenticator app",
"@totpCodePrompt": {
"description": "The instructional text for submitting a TOTP pass code"
}
},
"selectEmail": "Email",
"@selectEmail": {
"description": "Label for the radio button to select email as the user's chosen MFA method."
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ class TitleResolver extends Resolver<AuthenticatorStep> {
.confirmSignInWithTotpMfaCode;
}

/// The title for the confirm sign in (email MFA code) Widget.
String confirmSignInWithEmailMfaCode(BuildContext context) {
return AuthenticatorLocalizations.titlesOf(context)
.confirmSignInWithEmailMfaCode;
}

/// The title for the continue sign in (email MFA setup) Widget.
String continueSignInWithEmailMfaSetup(BuildContext context) {
return AuthenticatorLocalizations.titlesOf(context)
.continueSignInWithEmailMfaSetup;
}

/// The title for the continue sign in (mfa setup selection) Widget.
String continueSignInWithMfaSetupSelection(BuildContext context) {
return AuthenticatorLocalizations.titlesOf(context)
.continueSignInWithMfaSetupSelection;
}

/// The title for the reset password Widget.
String resetPassword(BuildContext context) {
return AuthenticatorLocalizations.titlesOf(context).resetPassword;
Expand Down Expand Up @@ -81,6 +99,12 @@ class TitleResolver extends Resolver<AuthenticatorStep> {
return continueSignInWithTotpSetup(context);
case AuthenticatorStep.confirmSignInWithTotpMfaCode:
return confirmSignInWithTotpMfaCode(context);
case AuthenticatorStep.confirmSignInWithEmailMfaCode:
return confirmSignInWithEmailMfaCode(context);
case AuthenticatorStep.continueSignInWithEmailMfaSetup:
return continueSignInWithEmailMfaSetup(context);
case AuthenticatorStep.continueSignInWithMfaSetupSelection:
return continueSignInWithMfaSetupSelection(context);
case AuthenticatorStep.resetPassword:
return resetPassword(context);
case AuthenticatorStep.confirmResetPassword:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ 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.continueSignInWithEmailMfaSetup({Key? key})
: this(key: key, step: AuthenticatorStep.continueSignInWithEmailMfaSetup);

const AuthenticatorScreen.continueSignInWithMfaSetupSelection({Key? key})
: this(
key: key,
step: AuthenticatorStep.continueSignInWithMfaSetupSelection,
);

const AuthenticatorScreen.resetPassword({Key? key})
: this(key: key, step: AuthenticatorStep.resetPassword);

Expand Down Expand Up @@ -91,6 +103,9 @@ class AuthenticatorScreen extends StatelessAuthenticatorComponent {
case AuthenticatorStep.confirmResetPassword:
case AuthenticatorStep.verifyUser:
case AuthenticatorStep.confirmVerifyUser:
case AuthenticatorStep.confirmSignInWithEmailMfaCode:
case AuthenticatorStep.continueSignInWithEmailMfaSetup:
case AuthenticatorStep.continueSignInWithMfaSetupSelection:
child = _FormWrapperView(step: step);
case AuthenticatorStep.loading:
throw StateError('Invalid step: $this');
Expand Down Expand Up @@ -299,6 +314,9 @@ extension on AuthenticatorStep {
case AuthenticatorStep.verifyUser:
case AuthenticatorStep.confirmVerifyUser:
case AuthenticatorStep.loading:
case AuthenticatorStep.confirmSignInWithEmailMfaCode:
case AuthenticatorStep.continueSignInWithEmailMfaSetup:
case AuthenticatorStep.continueSignInWithMfaSetupSelection:
throw StateError('Invalid step: $this');
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,18 @@ class AuthenticatorState extends ChangeNotifier {
notifyListeners();
}

/// The value for the email MFA setup form field
///
/// This value will be used during continue email MFA setup
String get mfaEmail => _mfaEmail;

set mfaEmail(String value) {
_mfaEmail = value.trim();
notifyListeners();
}

String _mfaEmail = '';

MfaType? _selectedMfaMethod;

TotpSetupDetails? get totpSetupDetails {
Expand Down Expand Up @@ -395,6 +407,23 @@ class AuthenticatorState extends ChangeNotifier {
_setIsBusy(false);
}

/// Select MFA setup preference using the values for [selectedMfaMethod]
Future<void> continueSignInWithMfaSetupSelection() async {
if (!_formKey.currentState!.validate()) {
return;
}

_setIsBusy(true);

final confirm = AuthConfirmSignInData(
confirmationValue: _selectedMfaMethod!.name,
);

_authBloc.add(AuthConfirmSignIn(confirm));
await nextBlocEvent();
_setIsBusy(false);
}

/// Complete TOTP setup using the values for [confirmationCode]
/// and any user attributes.
Future<void> confirmTotp() async {
Expand All @@ -414,6 +443,40 @@ class AuthenticatorState extends ChangeNotifier {
_setIsBusy(false);
}

/// complete Email MFA setup using the values for [confirmationCode]
Future<void> confirmEmailMfa() async {
if (!_formKey.currentState!.validate()) {
return;
}

_setIsBusy(true);

final confirm = AuthConfirmSignInData(
confirmationValue: _confirmationCode.trim(),
);

_authBloc.add(AuthConfirmSignIn(confirm));
await nextBlocEvent();
_setIsBusy(false);
}

/// Complete MFA setup using the values for [confirmationCode]
Future<void> continueEmailMfaSetup() async {
if (!_formKey.currentState!.validate()) {
return;
}

_setIsBusy(true);

final confirm = AuthConfirmSignInData(
confirmationValue: _mfaEmail.trim(),
);

_authBloc.add(AuthConfirmSignIn(confirm));
await nextBlocEvent();
_setIsBusy(false);
}

/// Complete the force password change with [newPassword]
Future<void> confirmSignInNewPassword() async {
if (!_formKey.currentState!.validate()) {
Expand Down Expand Up @@ -644,6 +707,7 @@ class AuthenticatorState extends ChangeNotifier {
authAttributes.clear();
_publicChallengeParams.clear();
_selectedMfaMethod = null;
_mfaEmail = '';
}

void _resetFormKey() {
Expand Down
Loading

0 comments on commit 0500755

Please sign in to comment.