From 042aee73e877ded3bc60d3d468b634a437832e3c Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 27 Jan 2025 14:39:24 -0800 Subject: [PATCH 01/23] feat: add email mfa support to state machine --- .../ui/src/helpers/authenticator/facade.ts | 10 +++- .../authenticator/formFields/defaults.ts | 24 +++++++- .../ui/src/helpers/authenticator/getRoute.ts | 4 ++ .../ui/src/machines/authenticator/actions.ts | 23 ++++++-- .../machines/authenticator/actors/signIn.ts | 57 +++++++++++++++++++ .../machines/authenticator/actors/signUp.ts | 2 + .../ui/src/machines/authenticator/guards.ts | 12 ++++ .../ui/src/machines/authenticator/index.ts | 3 +- .../ui/src/machines/authenticator/types.ts | 6 ++ packages/ui/src/types/authenticator/form.ts | 6 +- 10 files changed, 138 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index ec9ee59b665..11d35ffdcfb 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -45,7 +45,9 @@ export type AuthenticatorRoute = | 'signIn' | 'signUp' | 'transition' - | 'verifyUser'; + | 'verifyUser' + | 'setupEmail' + | 'selectMfaType'; type AuthenticatorValidationErrors = ValidationError; export type AuthStatus = 'configuring' | 'authenticated' | 'unauthenticated'; @@ -64,6 +66,7 @@ interface AuthenticatorServiceContextFacade { user: AuthUser; username: string; validationErrors: AuthenticatorValidationErrors; + allowedMfaTypes: string[]; } type SendEventAlias = @@ -99,6 +102,7 @@ interface NextAuthenticatorServiceContextFacade { totpSecretCode: string | undefined; username: string | undefined; unverifiedUserAttributes: UnverifiedUserAttributes | undefined; + allowedMfaTypes: string[] | undefined; } interface NextAuthenticatorSendEventAliases @@ -179,6 +183,7 @@ export const getServiceContextFacade = ( totpSecretCode = null, unverifiedUserAttributes, username, + allowedMfaTypes, } = actorContext; const { socialProviders = [] } = state.context?.config ?? {}; @@ -224,6 +229,7 @@ export const getServiceContextFacade = ( user, username, validationErrors, + allowedMfaTypes, // @v6-migration-note // While most of the properties @@ -248,6 +254,7 @@ export const getNextServiceContextFacade = ( totpSecretCode, unverifiedUserAttributes, username, + allowedMfaTypes, } = actorContext; const { socialProviders: federatedProviders, loginMechanisms } = @@ -272,6 +279,7 @@ export const getNextServiceContextFacade = ( totpSecretCode, unverifiedUserAttributes, username, + allowedMfaTypes, }; }; diff --git a/packages/ui/src/helpers/authenticator/formFields/defaults.ts b/packages/ui/src/helpers/authenticator/formFields/defaults.ts index 7e52f5ca3ff..1cad4ee1e0f 100644 --- a/packages/ui/src/helpers/authenticator/formFields/defaults.ts +++ b/packages/ui/src/helpers/authenticator/formFields/defaults.ts @@ -1,7 +1,7 @@ /** * This file contains helpers that generate default formFields for each screen */ -import { getActorState } from '../actor'; +import { getActorContext, getActorState } from '../actor'; import { defaultFormFieldOptions } from '../constants'; import { isAuthFieldWithDefaults } from '../form'; import { @@ -166,6 +166,26 @@ const getForceNewPasswordFormFields = (state: AuthMachineState): FormFields => { return formField; }; +const getSelectMfaTypeFormFields = (state: AuthMachineState): FormFields => { + const { allowedMfaTypes = [] } = getActorContext(state) || {}; + + return { + mfa_type: { + label: 'Select MFA Type', + placeholder: 'Please select desired MFA type', + type: 'radio', + isRequired: true, + allowedMfaTypes: allowedMfaTypes, + }, + }; +}; + +const getSetupEmailFormFields = (_: AuthMachineState): FormFields => ({ + email: { + ...getDefaultFormField('email'), + }, +}); + /** Collect all the defaultFormFields getters */ export const defaultFormFieldsGetters: Record< FormFieldComponents, @@ -180,4 +200,6 @@ export const defaultFormFieldsGetters: Record< confirmResetPassword: getConfirmResetPasswordFormFields, confirmVerifyUser: getConfirmationCodeFormFields, setupTotp: getConfirmationCodeFormFields, + setupEmail: getSetupEmailFormFields, + selectMfaType: getSelectMfaTypeFormFields, }; diff --git a/packages/ui/src/helpers/authenticator/getRoute.ts b/packages/ui/src/helpers/authenticator/getRoute.ts index b2858926507..1cdc00768fb 100644 --- a/packages/ui/src/helpers/authenticator/getRoute.ts +++ b/packages/ui/src/helpers/authenticator/getRoute.ts @@ -57,6 +57,10 @@ export const getRoute = ( return 'verifyUser'; case actorState?.matches('confirmVerifyUserAttribute'): return 'confirmVerifyUser'; + case actorState?.matches('setupEmail'): + return 'setupEmail'; + case actorState?.matches('selectMfaType'): + return 'selectMfaType'; case state.matches('getCurrentUser'): case actorState?.matches('fetchUserAttributes'): /** diff --git a/packages/ui/src/machines/authenticator/actions.ts b/packages/ui/src/machines/authenticator/actions.ts index 18985d5109b..c70d3f30348 100644 --- a/packages/ui/src/machines/authenticator/actions.ts +++ b/packages/ui/src/machines/authenticator/actions.ts @@ -45,6 +45,12 @@ const setTotpSecretCode = assign({ }, }); +const setAllowedMfaTypes = assign({ + allowedMfaTypes: (_, { data }: AuthEvent) => { + return data.nextStep?.allowedMFATypes || []; + }, +}); + const setSignInStep = assign({ step: 'SIGN_IN' }); const setShouldVerifyUserAttributeStep = assign({ @@ -59,11 +65,17 @@ const setConfirmAttributeCompleteStep = assign({ const setChallengeName = assign({ challengeName: (_, { data }: AuthEvent): ChallengeName | undefined => { const { signInStep } = (data as SignInOutput).nextStep; - return signInStep === 'CONFIRM_SIGN_IN_WITH_SMS_CODE' - ? 'SMS_MFA' - : signInStep === 'CONFIRM_SIGN_IN_WITH_TOTP_CODE' - ? 'SOFTWARE_TOKEN_MFA' - : undefined; + + switch (signInStep) { + case 'CONFIRM_SIGN_IN_WITH_SMS_CODE': + return 'SMS_MFA'; + case 'CONFIRM_SIGN_IN_WITH_TOTP_CODE': + return 'SOFTWARE_TOKEN_MFA'; + case 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE': + return 'EMAIL_OTP'; + default: + return undefined; + } }, }); @@ -243,6 +255,7 @@ const ACTIONS: MachineOptions['actions'] = { setUsernameForgotPassword, setUsernameSignIn, setUsernameSignUp, + setAllowedMfaTypes, }; export default ACTIONS; diff --git a/packages/ui/src/machines/authenticator/actors/signIn.ts b/packages/ui/src/machines/authenticator/actors/signIn.ts index 297254dafdc..16228a99295 100644 --- a/packages/ui/src/machines/authenticator/actors/signIn.ts +++ b/packages/ui/src/machines/authenticator/actors/signIn.ts @@ -49,6 +49,7 @@ const handleSignInResponse = { 'setMissingAttributes', 'setNextSignInStep', 'setTotpSecretCode', + 'setAllowedMfaTypes', ], target: '#signInActor.init', }, @@ -94,6 +95,14 @@ export function signInActor({ services }: SignInMachineOptions) { cond: 'shouldSetupTotp', target: 'setupTotp', }, + { + cond: 'shouldSetupEmail', + target: 'setupEmail', + }, + { + cond: 'shouldSelectMfaType', + target: 'selectMfaType', + }, { cond: ({ step }) => step === 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED', @@ -273,6 +282,44 @@ export function signInActor({ services }: SignInMachineOptions) { }, }, }, + setupEmail: { + initial: 'edit', + exit: ['clearFormValues', 'clearError', 'clearTouched'], + states: { + edit: { + entry: 'sendUpdate', + on: { + SUBMIT: { actions: 'handleSubmit', target: 'submit' }, + SIGN_IN: '#signInActor.signIn', + CHANGE: { actions: 'handleInput' }, + }, + }, + submit: { + tags: 'pending', + entry: ['sendUpdate', 'clearError'], + invoke: { src: 'handleSetupEmail', ...handleSignInResponse }, + }, + }, + }, + selectMfaType: { + initial: 'edit', + exit: ['clearFormValues', 'clearError', 'clearTouched'], + states: { + edit: { + entry: 'sendUpdate', + on: { + SUBMIT: { actions: 'handleSubmit', target: 'submit' }, + SIGN_IN: '#signInActor.signIn', + CHANGE: { actions: 'handleInput' }, + }, + }, + submit: { + tags: 'pending', + entry: ['sendUpdate', 'clearError'], + invoke: { src: 'handleSelectMfaType', ...handleSignInResponse }, + }, + }, + }, resolved: { type: 'final', data: (context): ActorDoneData => ({ @@ -307,6 +354,16 @@ export function signInActor({ services }: SignInMachineOptions) { const { confirmation_code: challengeResponse } = formValues; return services.handleConfirmSignIn({ challengeResponse }); }, + handleSetupEmail({ formValues }) { + return services.handleConfirmSignIn({ + challengeResponse: formValues.email, + }); + }, + handleSelectMfaType({ formValues }) { + return services.handleConfirmSignIn({ + challengeResponse: formValues.mfa_type, + }); + }, async handleForceChangePassword({ formValues }) { let { password: challengeResponse, diff --git a/packages/ui/src/machines/authenticator/actors/signUp.ts b/packages/ui/src/machines/authenticator/actors/signUp.ts index 7bf68785e64..d4a0e64a6c7 100644 --- a/packages/ui/src/machines/authenticator/actors/signUp.ts +++ b/packages/ui/src/machines/authenticator/actors/signUp.ts @@ -58,6 +58,7 @@ const handleAutoSignInResponse = { 'setChallengeName', 'setMissingAttributes', 'setTotpSecretCode', + 'setAllowedMfaTypes', ], target: '#signUpActor.resolved', }, @@ -266,6 +267,7 @@ export function signUpActor({ services }: SignUpMachineOptions) { totpSecretCode: context.totpSecretCode, username: context.username, unverifiedUserAttributes: context.unverifiedUserAttributes, + allowedMfaTypes: context.allowedMfaTypes, }), }, }, diff --git a/packages/ui/src/machines/authenticator/guards.ts b/packages/ui/src/machines/authenticator/guards.ts index 7a215bcb130..b2bcecb3164 100644 --- a/packages/ui/src/machines/authenticator/guards.ts +++ b/packages/ui/src/machines/authenticator/guards.ts @@ -12,6 +12,7 @@ import { AuthActorContext, AuthEvent } from './types'; const SIGN_IN_STEP_MFA_CONFIRMATION: string[] = [ 'CONFIRM_SIGN_IN_WITH_SMS_CODE', 'CONFIRM_SIGN_IN_WITH_TOTP_CODE', + 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', ]; // response next step guards @@ -71,6 +72,15 @@ const shouldConfirmSignIn = ({ step }: AuthActorContext) => const shouldSetupTotp = ({ step }: AuthActorContext) => step === 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP'; +const shouldSetupEmail = ({ step }: AuthActorContext) => + step === 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP'; + +const shouldSelectMfaType = ({ step }: AuthActorContext) => + [ + 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION', + 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION', + ].includes(step); + const shouldResetPassword = ({ step }: AuthActorContext) => step === 'RESET_PASSWORD'; @@ -132,6 +142,8 @@ const GUARDS: MachineOptions['guards'] = { shouldResetPassword, shouldResetPasswordFromSignIn, shouldSetupTotp, + shouldSetupEmail, + shouldSelectMfaType, shouldVerifyAttribute, }; diff --git a/packages/ui/src/machines/authenticator/index.ts b/packages/ui/src/machines/authenticator/index.ts index c0c3993da35..6fcb5780b33 100644 --- a/packages/ui/src/machines/authenticator/index.ts +++ b/packages/ui/src/machines/authenticator/index.ts @@ -303,7 +303,7 @@ export function createAuthenticatorMachine( { cond: 'hasActor', actions: forwardTo(({ actorRef }) => actorRef) }, ]), setActorDoneData: assign({ - actorDoneData: (context, event): ActorDoneData => ({ + actorDoneData: (_, event): ActorDoneData => ({ challengeName: event.data.challengeName, codeDeliveryDetails: event.data.codeDeliveryDetails, missingAttributes: event.data.missingAttributes, @@ -312,6 +312,7 @@ export function createAuthenticatorMachine( step: event.data.step, totpSecretCode: event.data.totpSecretCode, unverifiedUserAttributes: event.data.unverifiedUserAttributes, + allowedMfaTypes: event.data.allowedMfaTypes, }), }), applyAmplifyConfig: assign({ diff --git a/packages/ui/src/machines/authenticator/types.ts b/packages/ui/src/machines/authenticator/types.ts index 85da14cf88b..aa490d9f9b0 100644 --- a/packages/ui/src/machines/authenticator/types.ts +++ b/packages/ui/src/machines/authenticator/types.ts @@ -19,6 +19,7 @@ import { defaultServices } from './defaultServices'; export type ChallengeName = | 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' + | 'EMAIL_OTP' | 'SELECT_MFA_TYPE' | 'MFA_SETUP' | 'PASSWORD_VERIFIER' @@ -114,6 +115,7 @@ export interface ActorDoneData { totpSecretCode?: string; username?: string; unverifiedUserAttributes?: UnverifiedUserAttributes; + allowedMfaTypes?: string[]; } /** @@ -146,6 +148,9 @@ export type SignInStep = | 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED' | 'CONFIRM_SIGN_UP' | 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP' + | 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP' + | 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION' + | 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION' | 'RESET_PASSWORD' | 'SIGN_IN_COMPLETE'; // 'DONE' @@ -181,6 +186,7 @@ interface BaseFormContext { step: Step; totpSecretCode?: string; unverifiedUserAttributes?: UnverifiedUserAttributes; + allowedMfaTypes?: string[]; // kept in memory for submission to relevnat APIs username?: string; diff --git a/packages/ui/src/types/authenticator/form.ts b/packages/ui/src/types/authenticator/form.ts index ff54f2dbf0d..6ea19faef24 100644 --- a/packages/ui/src/types/authenticator/form.ts +++ b/packages/ui/src/types/authenticator/form.ts @@ -19,7 +19,9 @@ export type FormFieldComponents = | 'confirmSignUp' | 'confirmVerifyUser' | 'forgotPassword' - | 'setupTotp'; + | 'setupTotp' + | 'setupEmail' + | 'selectMfaType'; /** * Used to customize form field attributes for each authenticator screen. @@ -68,6 +70,8 @@ export interface FormFieldOptions { autocomplete?: string; /** Whether the first character is auto-capitalized */ autocapitalize?: string; + /** Mfa Types Allowed For Setup or Selection */ + allowedMfaTypes?: string[]; } export interface LegacyFormFieldOptions extends FormFieldOptions { From 6b796dd699f1817090a4e0dee9c3ff867e6d3622 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 27 Jan 2025 15:08:14 -0800 Subject: [PATCH 02/23] refactor: facade types --- packages/ui/src/helpers/authenticator/facade.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index 11d35ffdcfb..439833d8107 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -66,7 +66,7 @@ interface AuthenticatorServiceContextFacade { user: AuthUser; username: string; validationErrors: AuthenticatorValidationErrors; - allowedMfaTypes: string[]; + allowedMfaTypes?: string[] | undefined; } type SendEventAlias = @@ -102,7 +102,7 @@ interface NextAuthenticatorServiceContextFacade { totpSecretCode: string | undefined; username: string | undefined; unverifiedUserAttributes: UnverifiedUserAttributes | undefined; - allowedMfaTypes: string[] | undefined; + allowedMfaTypes?: string[] | undefined; } interface NextAuthenticatorSendEventAliases From e6a04737cf4c5ed256ada2f4d4c8daccd88bdf4e Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 27 Jan 2025 15:43:56 -0800 Subject: [PATCH 03/23] refactor: update facade types --- packages/ui/src/helpers/authenticator/facade.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index 439833d8107..aed8ab9ada5 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -66,7 +66,7 @@ interface AuthenticatorServiceContextFacade { user: AuthUser; username: string; validationErrors: AuthenticatorValidationErrors; - allowedMfaTypes?: string[] | undefined; + allowedMfaTypes: string[] | undefined; } type SendEventAlias = @@ -102,7 +102,7 @@ interface NextAuthenticatorServiceContextFacade { totpSecretCode: string | undefined; username: string | undefined; unverifiedUserAttributes: UnverifiedUserAttributes | undefined; - allowedMfaTypes?: string[] | undefined; + allowedMfaTypes: string[] | undefined; } interface NextAuthenticatorSendEventAliases From 8da9d38a870b590f18779fa645e2d9ec6386e36c Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 27 Jan 2025 16:07:01 -0800 Subject: [PATCH 04/23] test: update react core tests to align with type changes --- .../hooks/useAuthenticator/__mock__/useAuthenticator.ts | 3 ++- .../__tests__/__snapshots__/useAuthenticator.spec.tsx.snap | 4 ++++ .../useAuthenticator/__tests__/useAuthenticator.spec.tsx | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts index 03e035a20ff..e3121e372f7 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts @@ -30,6 +30,7 @@ const updateBlur = jest.fn(); const updateForm = jest.fn(); const user = { username: 'username', userId: 'userId' }; const validationErrors = {}; +const allowedMfaTypes = ['EMAIL', 'TOTP']; export const mockMachineContext: AuthenticatorMachineContext = { authStatus, @@ -53,7 +54,7 @@ export const mockMachineContext: AuthenticatorMachineContext = { toFederatedSignIn, toForgotPassword, totpSecretCode, - + allowedMfaTypes, unverifiedUserAttributes, username: 'george', validationErrors, diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap index 7ac510c2a23..8254d77469e 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/__snapshots__/useAuthenticator.spec.tsx.snap @@ -3,6 +3,10 @@ exports[`useAuthenticator returns the expected values 1`] = ` { "QRFields": null, + "allowedMfaTypes": [ + "EMAIL", + "TOTP", + ], "authStatus": "authenticated", "challengeName": "SELECT_MFA_TYPE", "codeDeliveryDetails": {}, diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/useAuthenticator.spec.tsx b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/useAuthenticator.spec.tsx index e17bd60bb93..be07993f921 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/useAuthenticator.spec.tsx +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__tests__/useAuthenticator.spec.tsx @@ -37,6 +37,7 @@ const mockServiceFacade: AuthenticatorServiceFacade = { toSignIn: jest.fn(), toSignUp: jest.fn(), skipVerification: jest.fn(), + allowedMfaTypes: ['EMAIL', 'TOTP'], }; const getServiceFacadeSpy = jest From 859c353841891af4cb98ca85bc8a73a0f91fb403 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 27 Jan 2025 16:09:04 -0800 Subject: [PATCH 05/23] refactor: generalize radio options --- packages/ui/src/helpers/authenticator/formFields/defaults.ts | 2 +- packages/ui/src/types/authenticator/form.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/formFields/defaults.ts b/packages/ui/src/helpers/authenticator/formFields/defaults.ts index 1cad4ee1e0f..bad7bc2871c 100644 --- a/packages/ui/src/helpers/authenticator/formFields/defaults.ts +++ b/packages/ui/src/helpers/authenticator/formFields/defaults.ts @@ -175,7 +175,7 @@ const getSelectMfaTypeFormFields = (state: AuthMachineState): FormFields => { placeholder: 'Please select desired MFA type', type: 'radio', isRequired: true, - allowedMfaTypes: allowedMfaTypes, + radioOptions: allowedMfaTypes, }, }; }; diff --git a/packages/ui/src/types/authenticator/form.ts b/packages/ui/src/types/authenticator/form.ts index 6ea19faef24..6ecdd41fa67 100644 --- a/packages/ui/src/types/authenticator/form.ts +++ b/packages/ui/src/types/authenticator/form.ts @@ -70,8 +70,8 @@ export interface FormFieldOptions { autocomplete?: string; /** Whether the first character is auto-capitalized */ autocapitalize?: string; - /** Mfa Types Allowed For Setup or Selection */ - allowedMfaTypes?: string[]; + /** Options for radio input groups*/ + radioOptions?: string[]; } export interface LegacyFormFieldOptions extends FormFieldOptions { From edefdb0e0a4957bb46086f5d047b26bb473ca191 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 27 Jan 2025 16:22:42 -0800 Subject: [PATCH 06/23] refactor: update react-core-auth tests --- .../Authenticator/context/Machine/__mock__/useMachine.ts | 2 ++ .../context/Machine/__tests__/MachineContext.spec.tsx | 1 + .../__tests__/__snapshots__/MachineContext.spec.tsx.snap | 1 + 3 files changed, 4 insertions(+) diff --git a/packages/react-core-auth/src/components/Authenticator/context/Machine/__mock__/useMachine.ts b/packages/react-core-auth/src/components/Authenticator/context/Machine/__mock__/useMachine.ts index 348346e99e0..2371a0162aa 100644 --- a/packages/react-core-auth/src/components/Authenticator/context/Machine/__mock__/useMachine.ts +++ b/packages/react-core-auth/src/components/Authenticator/context/Machine/__mock__/useMachine.ts @@ -14,6 +14,7 @@ const skipAttributeVerification = jest.fn(); const toFederatedSignIn = jest.fn(); const totpSecretCode = undefined; const unverifiedUserAttributes = {}; +const allowedMfaTypes = undefined; export const mockMachineContext: NextAuthenticatorServiceFacade = { challengeName, @@ -31,6 +32,7 @@ export const mockMachineContext: NextAuthenticatorServiceFacade = { totpSecretCode, unverifiedUserAttributes, username: 'Charles', + allowedMfaTypes, }; export const mockUseMachineOutput: UseMachine = mockMachineContext; diff --git a/packages/react-core-auth/src/components/Authenticator/context/Machine/__tests__/MachineContext.spec.tsx b/packages/react-core-auth/src/components/Authenticator/context/Machine/__tests__/MachineContext.spec.tsx index 6d7deed03b9..c466442f6eb 100644 --- a/packages/react-core-auth/src/components/Authenticator/context/Machine/__tests__/MachineContext.spec.tsx +++ b/packages/react-core-auth/src/components/Authenticator/context/Machine/__tests__/MachineContext.spec.tsx @@ -27,6 +27,7 @@ const mockServiceFacade: NextAuthenticatorServiceFacade = { totpSecretCode: undefined, unverifiedUserAttributes: { email: 'test#example.com' }, username: undefined, + allowedMfaTypes: undefined, }; const getNextServiceFacadeSpy = jest diff --git a/packages/react-core-auth/src/components/Authenticator/context/Machine/__tests__/__snapshots__/MachineContext.spec.tsx.snap b/packages/react-core-auth/src/components/Authenticator/context/Machine/__tests__/__snapshots__/MachineContext.spec.tsx.snap index 691231c1cee..afb5efc5f9a 100644 --- a/packages/react-core-auth/src/components/Authenticator/context/Machine/__tests__/__snapshots__/MachineContext.spec.tsx.snap +++ b/packages/react-core-auth/src/components/Authenticator/context/Machine/__tests__/__snapshots__/MachineContext.spec.tsx.snap @@ -2,6 +2,7 @@ exports[`useMachine returns the expected values 1`] = ` { + "allowedMfaTypes": undefined, "challengeName": undefined, "codeDeliveryDetails": undefined, "errorMessage": undefined, From 6a0bf4c3e1226a13ca123c5a0ad5ca8472b2fd56 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 27 Jan 2025 16:38:48 -0800 Subject: [PATCH 07/23] refactor: update vue test spec --- packages/vue/src/composables/__mocks__/useAuthenticatorMock.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vue/src/composables/__mocks__/useAuthenticatorMock.ts b/packages/vue/src/composables/__mocks__/useAuthenticatorMock.ts index 5e526716f7c..9cad78d7219 100644 --- a/packages/vue/src/composables/__mocks__/useAuthenticatorMock.ts +++ b/packages/vue/src/composables/__mocks__/useAuthenticatorMock.ts @@ -28,4 +28,5 @@ export const baseMockServiceFacade: UseAuthenticator = { username: 'tobias', validationErrors: {} as unknown as AuthenticatorServiceFacade['validationErrors'], + allowedMfaTypes: undefined, }; From 3dd8e97fbfeb075b61b585db6635b5e413c7ef16 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 27 Jan 2025 16:58:22 -0800 Subject: [PATCH 08/23] chore: add addtnl challenge names --- packages/ui/src/machines/authenticator/actions.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ui/src/machines/authenticator/actions.ts b/packages/ui/src/machines/authenticator/actions.ts index c70d3f30348..79de9656d2a 100644 --- a/packages/ui/src/machines/authenticator/actions.ts +++ b/packages/ui/src/machines/authenticator/actions.ts @@ -73,6 +73,12 @@ const setChallengeName = assign({ return 'SOFTWARE_TOKEN_MFA'; case 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE': return 'EMAIL_OTP'; + case 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION': + case 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP': + case 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP': + return 'MFA_SETUP'; + case 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION': + return 'SELECT_MFA_TYPE'; default: return undefined; } From a379df3c1669966589d1bbb571f0d5b125a0424c Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Wed, 29 Jan 2025 15:34:34 -0800 Subject: [PATCH 09/23] chore: quick changes --- packages/ui/src/machines/authenticator/actions.ts | 2 +- packages/ui/src/types/authenticator/form.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/machines/authenticator/actions.ts b/packages/ui/src/machines/authenticator/actions.ts index 79de9656d2a..28c3fda6d4e 100644 --- a/packages/ui/src/machines/authenticator/actions.ts +++ b/packages/ui/src/machines/authenticator/actions.ts @@ -47,7 +47,7 @@ const setTotpSecretCode = assign({ const setAllowedMfaTypes = assign({ allowedMfaTypes: (_, { data }: AuthEvent) => { - return data.nextStep?.allowedMFATypes || []; + return data.nextStep?.allowedMFATypes; }, }); diff --git a/packages/ui/src/types/authenticator/form.ts b/packages/ui/src/types/authenticator/form.ts index 6ecdd41fa67..950d9c45a3c 100644 --- a/packages/ui/src/types/authenticator/form.ts +++ b/packages/ui/src/types/authenticator/form.ts @@ -70,7 +70,7 @@ export interface FormFieldOptions { autocomplete?: string; /** Whether the first character is auto-capitalized */ autocapitalize?: string; - /** Options for radio input groups*/ + /** Options for radio input groups */ radioOptions?: string[]; } From 274252ce1868c90c040fe3de5c8a7b7d252c9568 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Thu, 30 Jan 2025 17:20:33 -0800 Subject: [PATCH 10/23] fix: allow labelled radio options --- packages/ui/src/helpers/authenticator/formFields/defaults.ts | 3 ++- packages/ui/src/types/authenticator/form.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/formFields/defaults.ts b/packages/ui/src/helpers/authenticator/formFields/defaults.ts index bad7bc2871c..530c422a26d 100644 --- a/packages/ui/src/helpers/authenticator/formFields/defaults.ts +++ b/packages/ui/src/helpers/authenticator/formFields/defaults.ts @@ -175,7 +175,8 @@ const getSelectMfaTypeFormFields = (state: AuthMachineState): FormFields => { placeholder: 'Please select desired MFA type', type: 'radio', isRequired: true, - radioOptions: allowedMfaTypes, + // TODO - i18n + radioOptions: allowedMfaTypes.map((value) => ({ label: value, value })), }, }; }; diff --git a/packages/ui/src/types/authenticator/form.ts b/packages/ui/src/types/authenticator/form.ts index 950d9c45a3c..01b47a8ab6b 100644 --- a/packages/ui/src/types/authenticator/form.ts +++ b/packages/ui/src/types/authenticator/form.ts @@ -71,7 +71,7 @@ export interface FormFieldOptions { /** Whether the first character is auto-capitalized */ autocapitalize?: string; /** Options for radio input groups */ - radioOptions?: string[]; + radioOptions?: { label: string; value: string }[]; } export interface LegacyFormFieldOptions extends FormFieldOptions { From 31ae03b895419315dbb3f0eec5da278086614d2a Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Thu, 30 Jan 2025 18:51:22 -0800 Subject: [PATCH 11/23] chore: add i18n setup --- .../authenticator/formFields/defaults.ts | 8 +++++- .../ui/src/helpers/authenticator/textUtil.ts | 25 +++++++++++++++++++ .../authenticator/defaultTexts.ts | 3 +++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/helpers/authenticator/formFields/defaults.ts b/packages/ui/src/helpers/authenticator/formFields/defaults.ts index 530c422a26d..6af9704a694 100644 --- a/packages/ui/src/helpers/authenticator/formFields/defaults.ts +++ b/packages/ui/src/helpers/authenticator/formFields/defaults.ts @@ -1,6 +1,7 @@ /** * This file contains helpers that generate default formFields for each screen */ +import { authenticatorTextUtil } from '../textUtil'; import { getActorContext, getActorState } from '../actor'; import { defaultFormFieldOptions } from '../constants'; import { isAuthFieldWithDefaults } from '../form'; @@ -17,6 +18,8 @@ import { } from '../../../machines/authenticator/types'; import { getPrimaryAlias } from '../formFields/utils'; +const { getMfaTypeLabelByValue } = authenticatorTextUtil; + export const DEFAULT_COUNTRY_CODE = '+1'; /** Helper function that gets the default formField for given field name */ @@ -176,7 +179,10 @@ const getSelectMfaTypeFormFields = (state: AuthMachineState): FormFields => { type: 'radio', isRequired: true, // TODO - i18n - radioOptions: allowedMfaTypes.map((value) => ({ label: value, value })), + radioOptions: allowedMfaTypes.map((value) => ({ + label: getMfaTypeLabelByValue(value), + value, + })), }, }; }; diff --git a/packages/ui/src/helpers/authenticator/textUtil.ts b/packages/ui/src/helpers/authenticator/textUtil.ts index 173c1f5f807..79087d17fcd 100644 --- a/packages/ui/src/helpers/authenticator/textUtil.ts +++ b/packages/ui/src/helpers/authenticator/textUtil.ts @@ -11,6 +11,9 @@ import { AuthenticatorRoute } from './facade'; */ const getChallengeText = (challengeName?: ChallengeName): string => { switch (challengeName) { + // TODO - i18n + case 'EMAIL_OTP': + return translate(DefaultTexts.CONFIRM_EMAIL); case 'SMS_MFA': return translate(DefaultTexts.CONFIRM_SMS); case 'SOFTWARE_TOKEN_MFA': @@ -79,6 +82,24 @@ const getSignInWithFederationText = ( ); }; +/** + * SelectMfaType + */ +// TODO - i18n +const getSelectMfaTypeByChallengeName = ( + challengeName: ChallengeName +): string => { + if (challengeName === 'MFA_SETUP') { + return translate(DefaultTexts.MFA_SETUP_SELECTION); + } + + return translate(DefaultTexts.MFA_SELECTION); +}; +// TODO - i18n +const getMfaTypeLabelByValue = (value: string): string => { + return value; +}; + export const authenticatorTextUtil = { /** Shared */ getBackToSignInText: () => translate(DefaultTexts.BACK_SIGN_IN), @@ -138,6 +159,10 @@ export const authenticatorTextUtil = { /** FederatedSignIn */ getSignInWithFederationText, + /** SelectMfaType */ + getSelectMfaTypeByChallengeName, + getMfaTypeLabelByValue, + /** VerifyUser */ getSkipText: () => translate(DefaultTexts.SKIP), getVerifyText: () => translate(DefaultTexts.VERIFY), diff --git a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts index b040b9ceeda..b621372c463 100644 --- a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts +++ b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts @@ -17,6 +17,7 @@ export const defaultTexts = { CONFIRM_RESET_PASSWORD_HEADING: 'Reset your Password', CONFIRM_SIGNUP_HEADING: 'Confirm Sign Up', CONFIRM_SMS: 'Confirm SMS Code', + CONFIRM_EMAIL: 'Confirm Email Code', // If challenge name is not returned CONFIRM_MFA_DEFAULT: 'Confirm MFA Code', CONFIRM_TOTP: 'Confirm TOTP Code', @@ -47,6 +48,8 @@ export const defaultTexts = { LOADING: 'Loading', LOGIN_NAME: 'Username', MIDDLE_NAME: 'Middle Name', + MFA_SETUP_SELECTION: 'Multi-Factor Authentication Setup', + MFA_SELECTION: 'Multi-Factor Authentication', NAME: 'Name', NICKNAME: 'Nickname', NEW_PASSWORD: 'New password', From 6295fa8fc43a44861aa64a8f23a823731454ed43 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Thu, 30 Jan 2025 19:52:16 -0800 Subject: [PATCH 12/23] test: update textUtil tests --- .../src/helpers/authenticator/__tests__/textUtil.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts b/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts index 2fdbfc60076..6efe9c2118f 100644 --- a/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts +++ b/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts @@ -4,6 +4,12 @@ import { authenticatorTextUtil } from '../textUtil'; describe('authenticatorTextUtil', () => { describe('getChallengeText', () => { + it('returns the correct text for the "EMAIL_OTP" challenge', () => { + expect(authenticatorTextUtil.getChallengeText('EMAIL_OTP')).toEqual( + 'Confirm Email Code' + ); + }); + it('returns the correct text for the "SMS_MFA" challenge', () => { expect(authenticatorTextUtil.getChallengeText('SMS_MFA')).toEqual( 'Confirm SMS Code' @@ -117,6 +123,8 @@ describe('authenticatorTextUtil', () => { let result; if (name === 'getChallengeText') { result = fn.call(authenticatorTextUtil, 'SMS_MFA'); + } else if (name === 'getMfaTypeLabelByValue') { + result = fn.call(authenticatorTextUtil, 'EMAIL'); } else { result = fn.call(authenticatorTextUtil); } From 0bcace610cd953fe144b8894286c8108cb35671c Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Fri, 31 Jan 2025 09:34:56 -0800 Subject: [PATCH 13/23] chore: type safe MFA types --- .../__mock__/useAuthenticator.ts | 5 +++- .../authenticator/__tests__/textUtil.test.ts | 29 ++++++++++++++++++- .../ui/src/helpers/authenticator/facade.ts | 5 ++-- .../ui/src/helpers/authenticator/textUtil.ts | 3 +- .../ui/src/machines/authenticator/types.ts | 7 +++-- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts index e3121e372f7..afce19f6a68 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts @@ -30,7 +30,10 @@ const updateBlur = jest.fn(); const updateForm = jest.fn(); const user = { username: 'username', userId: 'userId' }; const validationErrors = {}; -const allowedMfaTypes = ['EMAIL', 'TOTP']; +const allowedMfaTypes = [ + 'EMAIL', + 'TOTP', +] as AuthenticatorMachineContext['allowedMfaTypes']; export const mockMachineContext: AuthenticatorMachineContext = { authStatus, diff --git a/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts b/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts index 6efe9c2118f..d3ffef101cf 100644 --- a/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts +++ b/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts @@ -1,4 +1,7 @@ -import { V6AuthDeliveryMedium } from '../../../machines/authenticator/types'; +import { + AuthMFAType, + V6AuthDeliveryMedium, +} from '../../../machines/authenticator/types'; import { authenticatorTextUtil } from '../textUtil'; @@ -117,6 +120,30 @@ describe('authenticatorTextUtil', () => { }); }); + describe('getSelectMfaTypeByChallengeName', () => { + it('returns the correct text when challengeName is MFA_SETUP', () => { + expect( + authenticatorTextUtil.getSelectMfaTypeByChallengeName('MFA_SETUP') + ).toEqual('Multi-Factor Authentication Setup'); + }); + it('returns the correct text when challengeName is SELECT_MFA_TYPE', () => { + expect( + authenticatorTextUtil.getSelectMfaTypeByChallengeName('SELECT_MFA_TYPE') + ).toEqual('Multi-Factor Authentication'); + }); + }); + + describe('getMfaTypeLabelByValue', () => { + it.each(['EMAIL', 'SMS', 'TOTP'] as AuthMFAType[])( + 'returns the correct text when value is %s', + (value) => { + expect(authenticatorTextUtil.getMfaTypeLabelByValue(value)).toEqual( + value + ); + } + ); + }); + describe('authenticator shared text', () => { it('return a text for all the utils', () => { Object.entries(authenticatorTextUtil).map(([name, fn]) => { diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index aed8ab9ada5..ca1e1002bce 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -17,6 +17,7 @@ import { import { AuthActorContext, + AuthMFAType, AuthEvent, AuthEventData, AuthEventTypes, @@ -66,7 +67,7 @@ interface AuthenticatorServiceContextFacade { user: AuthUser; username: string; validationErrors: AuthenticatorValidationErrors; - allowedMfaTypes: string[] | undefined; + allowedMfaTypes: AuthMFAType[] | undefined; } type SendEventAlias = @@ -102,7 +103,7 @@ interface NextAuthenticatorServiceContextFacade { totpSecretCode: string | undefined; username: string | undefined; unverifiedUserAttributes: UnverifiedUserAttributes | undefined; - allowedMfaTypes: string[] | undefined; + allowedMfaTypes: AuthMFAType[] | undefined; } interface NextAuthenticatorSendEventAliases diff --git a/packages/ui/src/helpers/authenticator/textUtil.ts b/packages/ui/src/helpers/authenticator/textUtil.ts index 79087d17fcd..e91c45499a0 100644 --- a/packages/ui/src/helpers/authenticator/textUtil.ts +++ b/packages/ui/src/helpers/authenticator/textUtil.ts @@ -1,5 +1,6 @@ import { SocialProvider } from '../../types'; import { + AuthMFAType, ChallengeName, V5CodeDeliveryDetails, } from '../../machines/authenticator/types'; @@ -96,7 +97,7 @@ const getSelectMfaTypeByChallengeName = ( return translate(DefaultTexts.MFA_SELECTION); }; // TODO - i18n -const getMfaTypeLabelByValue = (value: string): string => { +const getMfaTypeLabelByValue = (value: AuthMFAType): string => { return value; }; diff --git a/packages/ui/src/machines/authenticator/types.ts b/packages/ui/src/machines/authenticator/types.ts index aa490d9f9b0..0afec0c6197 100644 --- a/packages/ui/src/machines/authenticator/types.ts +++ b/packages/ui/src/machines/authenticator/types.ts @@ -29,6 +29,9 @@ export type ChallengeName = | 'ADMIN_NO_SRP_AUTH' | 'NEW_PASSWORD_REQUIRED'; +// JS v6 Mfa Types +export type AuthMFAType = 'SMS' | 'TOTP' | 'EMAIL'; + /** * `AuthDeliveryMedium` is deeply nested in the v6 types, added this as utility */ @@ -115,7 +118,7 @@ export interface ActorDoneData { totpSecretCode?: string; username?: string; unverifiedUserAttributes?: UnverifiedUserAttributes; - allowedMfaTypes?: string[]; + allowedMfaTypes?: AuthMFAType[]; } /** @@ -186,7 +189,7 @@ interface BaseFormContext { step: Step; totpSecretCode?: string; unverifiedUserAttributes?: UnverifiedUserAttributes; - allowedMfaTypes?: string[]; + allowedMfaTypes?: AuthMFAType[]; // kept in memory for submission to relevnat APIs username?: string; From cc749cf6cae2ad741c19684336ace4e693b7087d Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 10 Feb 2025 17:11:00 -0800 Subject: [PATCH 14/23] fix: add addtl text utils --- packages/ui/src/helpers/authenticator/textUtil.ts | 3 +++ .../ui/src/i18n/dictionaries/authenticator/defaultTexts.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/ui/src/helpers/authenticator/textUtil.ts b/packages/ui/src/helpers/authenticator/textUtil.ts index e91c45499a0..d2617634c84 100644 --- a/packages/ui/src/helpers/authenticator/textUtil.ts +++ b/packages/ui/src/helpers/authenticator/textUtil.ts @@ -147,6 +147,9 @@ export const authenticatorTextUtil = { /** ForgotPassword */ getResetYourPasswordText: () => translate(DefaultTexts.RESET_PASSWORD), + /** SetupEmail */ + getSetupEmailText: () => translate(DefaultTexts.SETUP_EMAIL), + /** SetupTotp */ getSetupTotpText: () => translate(DefaultTexts.SETUP_TOTP), // TODO: add defaultText for below diff --git a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts index b621372c463..8b3d3e175fb 100644 --- a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts +++ b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts @@ -63,6 +63,7 @@ export const defaultTexts = { RESET_PASSWORD: 'Reset Password', SEND_CODE: 'Send code', SENDING: 'Sending', + SETUP_EMAIL: 'Setup Email', SETUP_TOTP: 'Setup TOTP', SHOW_PASSWORD: 'Show password', SIGN_IN_BUTTON: 'Sign in', From 28db8e6931bcb63dd4124bf7bc6f119230974767 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 18 Feb 2025 10:15:54 -0800 Subject: [PATCH 15/23] chore: add translation sites --- .../authenticator/__tests__/textUtil.test.ts | 14 ++++++++++---- .../authenticator/formFields/defaults.ts | 8 +++++--- .../ui/src/helpers/authenticator/textUtil.ts | 18 +++++++++++++----- .../dictionaries/authenticator/defaultTexts.ts | 5 +++++ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts b/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts index d3ffef101cf..814fbef60de 100644 --- a/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts +++ b/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts @@ -134,11 +134,17 @@ describe('authenticatorTextUtil', () => { }); describe('getMfaTypeLabelByValue', () => { - it.each(['EMAIL', 'SMS', 'TOTP'] as AuthMFAType[])( + const getMfaTypeLabelByValueTestCases: [AuthMFAType, string][] = [ + ['EMAIL', 'Email Message (EMAIL)'], + ['SMS', 'Text Message (SMS)'], + ['TOTP', 'Authenticator App (TOTP)'], + ]; + + it.each(getMfaTypeLabelByValueTestCases)( 'returns the correct text when value is %s', - (value) => { - expect(authenticatorTextUtil.getMfaTypeLabelByValue(value)).toEqual( - value + (input, output) => { + expect(authenticatorTextUtil.getMfaTypeLabelByValue(input)).toEqual( + output ); } ); diff --git a/packages/ui/src/helpers/authenticator/formFields/defaults.ts b/packages/ui/src/helpers/authenticator/formFields/defaults.ts index 6af9704a694..ed21c3cda38 100644 --- a/packages/ui/src/helpers/authenticator/formFields/defaults.ts +++ b/packages/ui/src/helpers/authenticator/formFields/defaults.ts @@ -17,6 +17,7 @@ import { SignInState, } from '../../../machines/authenticator/types'; import { getPrimaryAlias } from '../formFields/utils'; +import { defaultTexts } from '../../../i18n/dictionaries'; const { getMfaTypeLabelByValue } = authenticatorTextUtil; @@ -174,11 +175,12 @@ const getSelectMfaTypeFormFields = (state: AuthMachineState): FormFields => { return { mfa_type: { - label: 'Select MFA Type', - placeholder: 'Please select desired MFA type', + label: defaultTexts.SELECT_MFA_TYPE_LABEL, + placeholder: defaultTexts.SELECT_MFA_TYPE_PLACEHOLDER, type: 'radio', isRequired: true, - // TODO - i18n + // translation applied here + // `applyTransformation` only translates label, placeholder radioOptions: allowedMfaTypes.map((value) => ({ label: getMfaTypeLabelByValue(value), value, diff --git a/packages/ui/src/helpers/authenticator/textUtil.ts b/packages/ui/src/helpers/authenticator/textUtil.ts index d2617634c84..3990bd8451b 100644 --- a/packages/ui/src/helpers/authenticator/textUtil.ts +++ b/packages/ui/src/helpers/authenticator/textUtil.ts @@ -6,13 +6,13 @@ import { } from '../../machines/authenticator/types'; import { translate, DefaultTexts } from '../../i18n'; import { AuthenticatorRoute } from './facade'; +import { defaultTexts } from '../../i18n/dictionaries'; /** * ConfirmSignIn */ const getChallengeText = (challengeName?: ChallengeName): string => { switch (challengeName) { - // TODO - i18n case 'EMAIL_OTP': return translate(DefaultTexts.CONFIRM_EMAIL); case 'SMS_MFA': @@ -86,7 +86,6 @@ const getSignInWithFederationText = ( /** * SelectMfaType */ -// TODO - i18n const getSelectMfaTypeByChallengeName = ( challengeName: ChallengeName ): string => { @@ -96,9 +95,18 @@ const getSelectMfaTypeByChallengeName = ( return translate(DefaultTexts.MFA_SELECTION); }; -// TODO - i18n -const getMfaTypeLabelByValue = (value: AuthMFAType): string => { - return value; + +const getMfaTypeLabelByValue = (mfaType: AuthMFAType): string => { + switch (mfaType) { + case 'EMAIL': + return translate(defaultTexts.EMAIL_OTP); + case 'SMS': + return translate(defaultTexts.SMS_MFA); + case 'TOTP': + return translate(defaultTexts.SOFTWARE_TOKEN_MFA); + default: + return translate(mfaType); + } }; export const authenticatorTextUtil = { diff --git a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts index 8b3d3e175fb..89617ff4b13 100644 --- a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts +++ b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts @@ -27,6 +27,7 @@ export const defaultTexts = { CREATE_ACCOUNT: 'Create Account', CREATING_ACCOUNT: 'Creating Account', EMAIL_ADDRESS: 'Email', + EMAIL_OTP: 'Email Message (EMAIL)', ENTER_BIRTHDATE: 'Enter your Birthdate', ENTER_CODE: 'Enter your code', ENTER_CONFIRMATION_CODE: 'Enter your Confirmation Code', @@ -61,6 +62,8 @@ export const defaultTexts = { RESEND_CODE: 'Resend Code', RESET_PASSWORD_HEADING: 'Reset your password', RESET_PASSWORD: 'Reset Password', + SELECT_MFA_TYPE_LABEL: 'Select MFA Type', + SELECT_MFA_TYPE_PLACEHOLDER: 'Please select desired MFA type', SEND_CODE: 'Send code', SENDING: 'Sending', SETUP_EMAIL: 'Setup Email', @@ -76,8 +79,10 @@ export const defaultTexts = { SIGN_UP_BUTTON: 'Create a new account', SIGNING_IN_BUTTON: 'Signing in', SKIP: 'Skip', + SMS_MFA: 'Text Message (SMS)', SUBMIT: 'Submit', SUBMITTING: 'Submitting', + SOFTWARE_TOKEN_MFA: 'Authenticator App (TOTP)', UPPERCASE_COPY: 'COPY', VERIFY_CONTACT: 'Verify Contact', VERIFY_HEADING: 'Account recovery requires verified contact information', From d55d2e0712fb122ba127e835519356232bfabe88 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 18 Feb 2025 10:20:27 -0800 Subject: [PATCH 16/23] chore: alphabetization --- .../hooks/useAuthenticator/__mock__/useAuthenticator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts index afce19f6a68..489cc7b7e14 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts @@ -4,6 +4,10 @@ import { } from '../../types'; import { UseAuthenticator } from '../types'; +const allowedMfaTypes = [ + 'EMAIL', + 'TOTP', +] as AuthenticatorMachineContext['allowedMfaTypes']; const authStatus = 'unauthenticated'; const challengeName = 'CUSTOM_CHALLENGE'; const codeDeliveryDetails = @@ -30,10 +34,6 @@ const updateBlur = jest.fn(); const updateForm = jest.fn(); const user = { username: 'username', userId: 'userId' }; const validationErrors = {}; -const allowedMfaTypes = [ - 'EMAIL', - 'TOTP', -] as AuthenticatorMachineContext['allowedMfaTypes']; export const mockMachineContext: AuthenticatorMachineContext = { authStatus, From 7e51784c21227173d83a2ed123be28bea8fb7c29 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 18 Feb 2025 10:21:41 -0800 Subject: [PATCH 17/23] chore: alphabetization --- .../hooks/useAuthenticator/__mock__/useAuthenticator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts index 489cc7b7e14..86139f009a8 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/__mock__/useAuthenticator.ts @@ -36,6 +36,7 @@ const user = { username: 'username', userId: 'userId' }; const validationErrors = {}; export const mockMachineContext: AuthenticatorMachineContext = { + allowedMfaTypes, authStatus, challengeName, codeDeliveryDetails, @@ -57,7 +58,6 @@ export const mockMachineContext: AuthenticatorMachineContext = { toFederatedSignIn, toForgotPassword, totpSecretCode, - allowedMfaTypes, unverifiedUserAttributes, username: 'george', validationErrors, From 88039f126b7d356348c3b0864fe1361170b97f60 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 18 Feb 2025 10:48:41 -0800 Subject: [PATCH 18/23] chore: remove comment --- packages/ui/src/helpers/authenticator/formFields/defaults.ts | 2 -- packages/ui/src/types/authenticator/attributes.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/formFields/defaults.ts b/packages/ui/src/helpers/authenticator/formFields/defaults.ts index ed21c3cda38..b9c0992d0a2 100644 --- a/packages/ui/src/helpers/authenticator/formFields/defaults.ts +++ b/packages/ui/src/helpers/authenticator/formFields/defaults.ts @@ -179,8 +179,6 @@ const getSelectMfaTypeFormFields = (state: AuthMachineState): FormFields => { placeholder: defaultTexts.SELECT_MFA_TYPE_PLACEHOLDER, type: 'radio', isRequired: true, - // translation applied here - // `applyTransformation` only translates label, placeholder radioOptions: allowedMfaTypes.map((value) => ({ label: getMfaTypeLabelByValue(value), value, diff --git a/packages/ui/src/types/authenticator/attributes.ts b/packages/ui/src/types/authenticator/attributes.ts index 00963d00d69..61a172e8ee9 100644 --- a/packages/ui/src/types/authenticator/attributes.ts +++ b/packages/ui/src/types/authenticator/attributes.ts @@ -64,6 +64,7 @@ export const authFieldsWithDefaults = [ 'confirmation_code', 'password', 'confirm_password', + 'mfa_type', ] as const; /** Input fields that we provide default fields with */ From 830d3eaa19a54ae1bec9b591970271cd6dedd927 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 18 Feb 2025 11:06:36 -0800 Subject: [PATCH 19/23] chore: alphabetization --- packages/ui/src/machines/authenticator/actions.ts | 2 +- packages/ui/src/types/authenticator/attributes.ts | 1 - packages/ui/src/types/authenticator/form.ts | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/machines/authenticator/actions.ts b/packages/ui/src/machines/authenticator/actions.ts index 28c3fda6d4e..49b592bd7db 100644 --- a/packages/ui/src/machines/authenticator/actions.ts +++ b/packages/ui/src/machines/authenticator/actions.ts @@ -242,6 +242,7 @@ const ACTIONS: MachineOptions['actions'] = { handleBlur, handleInput, handleSubmit, + setAllowedMfaTypes, setChallengeName, setCodeDeliveryDetails, setFieldErrors, @@ -261,7 +262,6 @@ const ACTIONS: MachineOptions['actions'] = { setUsernameForgotPassword, setUsernameSignIn, setUsernameSignUp, - setAllowedMfaTypes, }; export default ACTIONS; diff --git a/packages/ui/src/types/authenticator/attributes.ts b/packages/ui/src/types/authenticator/attributes.ts index 61a172e8ee9..00963d00d69 100644 --- a/packages/ui/src/types/authenticator/attributes.ts +++ b/packages/ui/src/types/authenticator/attributes.ts @@ -64,7 +64,6 @@ export const authFieldsWithDefaults = [ 'confirmation_code', 'password', 'confirm_password', - 'mfa_type', ] as const; /** Input fields that we provide default fields with */ diff --git a/packages/ui/src/types/authenticator/form.ts b/packages/ui/src/types/authenticator/form.ts index 01b47a8ab6b..656c8fb74a2 100644 --- a/packages/ui/src/types/authenticator/form.ts +++ b/packages/ui/src/types/authenticator/form.ts @@ -19,9 +19,9 @@ export type FormFieldComponents = | 'confirmSignUp' | 'confirmVerifyUser' | 'forgotPassword' - | 'setupTotp' | 'setupEmail' - | 'selectMfaType'; + | 'selectMfaType' + | 'setupTotp'; /** * Used to customize form field attributes for each authenticator screen. From 255403bb1c4119dae81f87b5dfafcf906a85bad9 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 18 Feb 2025 13:37:05 -0800 Subject: [PATCH 20/23] chore: alphabetization --- packages/ui/src/helpers/authenticator/facade.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index ca1e1002bce..a7f839cfc69 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -17,11 +17,11 @@ import { import { AuthActorContext, - AuthMFAType, AuthEvent, AuthEventData, AuthEventTypes, AuthMachineState, + AuthMFAType, ChallengeName, NavigableRoute, V5CodeDeliveryDetails, From 9f703293489c19337bd8558517dddde5b6a55cd4 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Fri, 21 Feb 2025 08:57:10 -0800 Subject: [PATCH 21/23] chore: address feedback --- .../authenticator/__tests__/textUtil.test.ts | 2 +- .../ui/src/helpers/authenticator/facade.ts | 8 +- .../authenticator/formFields/defaults.ts | 26 ++--- .../ui/src/helpers/authenticator/textUtil.ts | 3 +- .../authenticator/defaultTexts.ts | 5 +- .../machines/authenticator/actors/signIn.ts | 100 ++++++------------ .../machines/authenticator/actors/utils.ts | 21 ++++ .../ui/src/machines/authenticator/types.ts | 2 +- packages/ui/src/types/authenticator/form.ts | 4 +- 9 files changed, 69 insertions(+), 102 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts b/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts index 814fbef60de..251e73404b1 100644 --- a/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts +++ b/packages/ui/src/helpers/authenticator/__tests__/textUtil.test.ts @@ -135,7 +135,7 @@ describe('authenticatorTextUtil', () => { describe('getMfaTypeLabelByValue', () => { const getMfaTypeLabelByValueTestCases: [AuthMFAType, string][] = [ - ['EMAIL', 'Email Message (EMAIL)'], + ['EMAIL', 'Email Message'], ['SMS', 'Text Message (SMS)'], ['TOTP', 'Authenticator App (TOTP)'], ]; diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index a7f839cfc69..aa2047a39b3 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -42,13 +42,13 @@ export type AuthenticatorRoute = | 'forgotPassword' | 'setup' | 'signOut' + | 'selectMfaType' + | 'setupEmail' | 'setupTotp' | 'signIn' | 'signUp' | 'transition' - | 'verifyUser' - | 'setupEmail' - | 'selectMfaType'; + | 'verifyUser'; type AuthenticatorValidationErrors = ValidationError; export type AuthStatus = 'configuring' | 'authenticated' | 'unauthenticated'; @@ -177,6 +177,7 @@ export const getServiceContextFacade = ( ): AuthenticatorServiceContextFacade => { const actorContext = (getActorContext(state) ?? {}) as AuthActorContext; const { + allowedMfaTypes, challengeName, codeDeliveryDetails, remoteError: error, @@ -184,7 +185,6 @@ export const getServiceContextFacade = ( totpSecretCode = null, unverifiedUserAttributes, username, - allowedMfaTypes, } = actorContext; const { socialProviders = [] } = state.context?.config ?? {}; diff --git a/packages/ui/src/helpers/authenticator/formFields/defaults.ts b/packages/ui/src/helpers/authenticator/formFields/defaults.ts index b9c0992d0a2..aa93d63556b 100644 --- a/packages/ui/src/helpers/authenticator/formFields/defaults.ts +++ b/packages/ui/src/helpers/authenticator/formFields/defaults.ts @@ -1,8 +1,7 @@ /** * This file contains helpers that generate default formFields for each screen */ -import { authenticatorTextUtil } from '../textUtil'; -import { getActorContext, getActorState } from '../actor'; +import { getActorState } from '../actor'; import { defaultFormFieldOptions } from '../constants'; import { isAuthFieldWithDefaults } from '../form'; import { @@ -17,9 +16,6 @@ import { SignInState, } from '../../../machines/authenticator/types'; import { getPrimaryAlias } from '../formFields/utils'; -import { defaultTexts } from '../../../i18n/dictionaries'; - -const { getMfaTypeLabelByValue } = authenticatorTextUtil; export const DEFAULT_COUNTRY_CODE = '+1'; @@ -170,27 +166,19 @@ const getForceNewPasswordFormFields = (state: AuthMachineState): FormFields => { return formField; }; -const getSelectMfaTypeFormFields = (state: AuthMachineState): FormFields => { - const { allowedMfaTypes = [] } = getActorContext(state) || {}; - +const getSelectMfaTypeFormFields = (_: AuthMachineState): FormFields => { return { mfa_type: { - label: defaultTexts.SELECT_MFA_TYPE_LABEL, - placeholder: defaultTexts.SELECT_MFA_TYPE_PLACEHOLDER, + label: 'Select MFA Type', + placeholder: 'Please select desired MFA type', type: 'radio', isRequired: true, - radioOptions: allowedMfaTypes.map((value) => ({ - label: getMfaTypeLabelByValue(value), - value, - })), }, }; }; const getSetupEmailFormFields = (_: AuthMachineState): FormFields => ({ - email: { - ...getDefaultFormField('email'), - }, + email: getDefaultFormField('email'), }); /** Collect all the defaultFormFields getters */ @@ -206,7 +194,7 @@ export const defaultFormFieldsGetters: Record< forgotPassword: getForgotPasswordFormFields, confirmResetPassword: getConfirmResetPasswordFormFields, confirmVerifyUser: getConfirmationCodeFormFields, - setupTotp: getConfirmationCodeFormFields, - setupEmail: getSetupEmailFormFields, selectMfaType: getSelectMfaTypeFormFields, + setupEmail: getSetupEmailFormFields, + setupTotp: getConfirmationCodeFormFields, }; diff --git a/packages/ui/src/helpers/authenticator/textUtil.ts b/packages/ui/src/helpers/authenticator/textUtil.ts index 3990bd8451b..2da63e07a56 100644 --- a/packages/ui/src/helpers/authenticator/textUtil.ts +++ b/packages/ui/src/helpers/authenticator/textUtil.ts @@ -172,8 +172,9 @@ export const authenticatorTextUtil = { getSignInWithFederationText, /** SelectMfaType */ - getSelectMfaTypeByChallengeName, getMfaTypeLabelByValue, + getSelectMfaTypeByChallengeName, + getSelectMfaTypeText: () => translate(DefaultTexts.SELECT_MFA_TYPE), /** VerifyUser */ getSkipText: () => translate(DefaultTexts.SKIP), diff --git a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts index 89617ff4b13..4671ce26caa 100644 --- a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts +++ b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts @@ -27,7 +27,7 @@ export const defaultTexts = { CREATE_ACCOUNT: 'Create Account', CREATING_ACCOUNT: 'Creating Account', EMAIL_ADDRESS: 'Email', - EMAIL_OTP: 'Email Message (EMAIL)', + EMAIL_OTP: 'Email Message', ENTER_BIRTHDATE: 'Enter your Birthdate', ENTER_CODE: 'Enter your code', ENTER_CONFIRMATION_CODE: 'Enter your Confirmation Code', @@ -62,10 +62,9 @@ export const defaultTexts = { RESEND_CODE: 'Resend Code', RESET_PASSWORD_HEADING: 'Reset your password', RESET_PASSWORD: 'Reset Password', - SELECT_MFA_TYPE_LABEL: 'Select MFA Type', - SELECT_MFA_TYPE_PLACEHOLDER: 'Please select desired MFA type', SEND_CODE: 'Send code', SENDING: 'Sending', + SELECT_MFA_TYPE: 'Select MFA Type', SETUP_EMAIL: 'Setup Email', SETUP_TOTP: 'Setup TOTP', SHOW_PASSWORD: 'Show password', diff --git a/packages/ui/src/machines/authenticator/actors/signIn.ts b/packages/ui/src/machines/authenticator/actors/signIn.ts index 16228a99295..76cec85bca0 100644 --- a/packages/ui/src/machines/authenticator/actors/signIn.ts +++ b/packages/ui/src/machines/authenticator/actors/signIn.ts @@ -15,7 +15,10 @@ import guards from '../guards'; import { AuthEvent, ActorDoneData, SignInContext } from '../types'; -import { getFederatedSignInState } from './utils'; +import { + getConfirmSignInFormValuesKey, + getFederatedSignInState, +} from './utils'; export interface SignInMachineOptions { services?: Partial; @@ -78,6 +81,26 @@ const handleFetchUserAttributesResponse = { }, }; +const defaultState = { + initial: 'edit', + exit: ['clearFormValues', 'clearError', 'clearTouched'], + states: { + edit: { + entry: 'sendUpdate', + on: { + SUBMIT: { actions: 'handleSubmit', target: 'submit' }, + SIGN_IN: '#signInActor.signIn', + CHANGE: { actions: 'handleInput' }, + }, + }, + submit: { + tags: 'pending', + entry: ['sendUpdate', 'clearError'], + invoke: { src: 'confirmSignIn', ...handleSignInResponse }, + }, + }, +}; + export function signInActor({ services }: SignInMachineOptions) { return createMachine( { @@ -263,63 +286,9 @@ export function signInActor({ services }: SignInMachineOptions) { }, }, }, - setupTotp: { - initial: 'edit', - exit: ['clearFormValues', 'clearError', 'clearTouched'], - states: { - edit: { - entry: 'sendUpdate', - on: { - SUBMIT: { actions: 'handleSubmit', target: 'submit' }, - SIGN_IN: '#signInActor.signIn', - CHANGE: { actions: 'handleInput' }, - }, - }, - submit: { - tags: 'pending', - entry: ['sendUpdate', 'clearError'], - invoke: { src: 'confirmSignIn', ...handleSignInResponse }, - }, - }, - }, - setupEmail: { - initial: 'edit', - exit: ['clearFormValues', 'clearError', 'clearTouched'], - states: { - edit: { - entry: 'sendUpdate', - on: { - SUBMIT: { actions: 'handleSubmit', target: 'submit' }, - SIGN_IN: '#signInActor.signIn', - CHANGE: { actions: 'handleInput' }, - }, - }, - submit: { - tags: 'pending', - entry: ['sendUpdate', 'clearError'], - invoke: { src: 'handleSetupEmail', ...handleSignInResponse }, - }, - }, - }, - selectMfaType: { - initial: 'edit', - exit: ['clearFormValues', 'clearError', 'clearTouched'], - states: { - edit: { - entry: 'sendUpdate', - on: { - SUBMIT: { actions: 'handleSubmit', target: 'submit' }, - SIGN_IN: '#signInActor.signIn', - CHANGE: { actions: 'handleInput' }, - }, - }, - submit: { - tags: 'pending', - entry: ['sendUpdate', 'clearError'], - invoke: { src: 'handleSelectMfaType', ...handleSignInResponse }, - }, - }, - }, + setupTotp: defaultState, + setupEmail: defaultState, + selectMfaType: defaultState, resolved: { type: 'final', data: (context): ActorDoneData => ({ @@ -350,20 +319,11 @@ export function signInActor({ services }: SignInMachineOptions) { const { password } = formValues; return services.handleSignIn({ username, password }); }, - confirmSignIn({ formValues }) { - const { confirmation_code: challengeResponse } = formValues; + confirmSignIn({ formValues, step }) { + const formValuesKey = getConfirmSignInFormValuesKey(step); + const { [formValuesKey]: challengeResponse } = formValues; return services.handleConfirmSignIn({ challengeResponse }); }, - handleSetupEmail({ formValues }) { - return services.handleConfirmSignIn({ - challengeResponse: formValues.email, - }); - }, - handleSelectMfaType({ formValues }) { - return services.handleConfirmSignIn({ - challengeResponse: formValues.mfa_type, - }); - }, async handleForceChangePassword({ formValues }) { let { password: challengeResponse, diff --git a/packages/ui/src/machines/authenticator/actors/utils.ts b/packages/ui/src/machines/authenticator/actors/utils.ts index d0175b27f5a..64b70503bd3 100644 --- a/packages/ui/src/machines/authenticator/actors/utils.ts +++ b/packages/ui/src/machines/authenticator/actors/utils.ts @@ -1,3 +1,5 @@ +import { Step } from '../types'; + export const getFederatedSignInState = (target: 'signIn' | 'signUp') => ({ entry: ['sendUpdate', 'clearError'], invoke: { @@ -6,3 +8,22 @@ export const getFederatedSignInState = (target: 'signIn' | 'signUp') => ({ onError: { actions: 'setRemoteError', target }, }, }); + +export const getConfirmSignInFormValuesKey = ( + signInStep: Step +): 'confirmation_code' | 'mfa_type' | 'email' => { + if ( + [ + 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION', + 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION', + ].includes(signInStep) + ) { + return 'mfa_type'; + } + + if (signInStep === 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP') { + return 'email'; + } + + return 'confirmation_code'; +}; diff --git a/packages/ui/src/machines/authenticator/types.ts b/packages/ui/src/machines/authenticator/types.ts index 0afec0c6197..116578c04fa 100644 --- a/packages/ui/src/machines/authenticator/types.ts +++ b/packages/ui/src/machines/authenticator/types.ts @@ -171,7 +171,7 @@ export type UserAttributeStep = | 'CONFIRM_ATTRIBUTE_WITH_CODE' | 'CONFIRM_ATTRIBUTE_COMPLETE'; // 'DONE' -type Step = +export type Step = | InitialStep | SignInStep | SignUpStep diff --git a/packages/ui/src/types/authenticator/form.ts b/packages/ui/src/types/authenticator/form.ts index 656c8fb74a2..c1cd9f65b7c 100644 --- a/packages/ui/src/types/authenticator/form.ts +++ b/packages/ui/src/types/authenticator/form.ts @@ -19,8 +19,8 @@ export type FormFieldComponents = | 'confirmSignUp' | 'confirmVerifyUser' | 'forgotPassword' - | 'setupEmail' | 'selectMfaType' + | 'setupEmail' | 'setupTotp'; /** @@ -70,8 +70,6 @@ export interface FormFieldOptions { autocomplete?: string; /** Whether the first character is auto-capitalized */ autocapitalize?: string; - /** Options for radio input groups */ - radioOptions?: { label: string; value: string }[]; } export interface LegacyFormFieldOptions extends FormFieldOptions { From ba0aa219ef4812194a605dedc5170e0c9d68aa2c Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Fri, 21 Feb 2025 09:15:22 -0800 Subject: [PATCH 22/23] chore: address feedback --- .../ui/src/helpers/authenticator/facade.ts | 2 +- .../machines/authenticator/actors/signIn.ts | 57 ++++++++----------- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index aa2047a39b3..d5de31403ab 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -217,6 +217,7 @@ export const getServiceContextFacade = ( })(route); const facade = { + allowedMfaTypes, authStatus, challengeName, codeDeliveryDetails, @@ -230,7 +231,6 @@ export const getServiceContextFacade = ( user, username, validationErrors, - allowedMfaTypes, // @v6-migration-note // While most of the properties diff --git a/packages/ui/src/machines/authenticator/actors/signIn.ts b/packages/ui/src/machines/authenticator/actors/signIn.ts index 76cec85bca0..af356b7192b 100644 --- a/packages/ui/src/machines/authenticator/actors/signIn.ts +++ b/packages/ui/src/machines/authenticator/actors/signIn.ts @@ -81,9 +81,9 @@ const handleFetchUserAttributesResponse = { }, }; -const defaultState = { +const getDefaultConfirmSignInState = (exit: string[]) => ({ initial: 'edit', - exit: ['clearFormValues', 'clearError', 'clearTouched'], + exit, states: { edit: { entry: 'sendUpdate', @@ -99,7 +99,7 @@ const defaultState = { invoke: { src: 'confirmSignIn', ...handleSignInResponse }, }, }, -}; +}); export function signInActor({ services }: SignInMachineOptions) { return createMachine( @@ -187,33 +187,12 @@ export function signInActor({ services }: SignInMachineOptions) { }, }, }, - confirmSignIn: { - initial: 'edit', - exit: [ - 'clearChallengeName', - 'clearFormValues', - 'clearError', - 'clearTouched', - ], - states: { - edit: { - entry: 'sendUpdate', - on: { - SUBMIT: { actions: 'handleSubmit', target: 'submit' }, - SIGN_IN: '#signInActor.signIn', - CHANGE: { actions: 'handleInput' }, - }, - }, - submit: { - tags: 'pending', - entry: ['clearError', 'sendUpdate'], - invoke: { - src: 'confirmSignIn', - ...handleSignInResponse, - }, - }, - }, - }, + confirmSignIn: getDefaultConfirmSignInState([ + 'clearChallengeName', + 'clearFormValues', + 'clearError', + 'clearTouched', + ]), forceChangePassword: { entry: 'sendUpdate', type: 'parallel', @@ -286,9 +265,21 @@ export function signInActor({ services }: SignInMachineOptions) { }, }, }, - setupTotp: defaultState, - setupEmail: defaultState, - selectMfaType: defaultState, + setupTotp: getDefaultConfirmSignInState([ + 'clearFormValues', + 'clearError', + 'clearTouched', + ]), + setupEmail: getDefaultConfirmSignInState([ + 'clearFormValues', + 'clearError', + 'clearTouched', + ]), + selectMfaType: getDefaultConfirmSignInState([ + 'clearFormValues', + 'clearError', + 'clearTouched', + ]), resolved: { type: 'final', data: (context): ActorDoneData => ({ From d9ea3a6448061a4c16a0d95884d209972ac884a5 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Fri, 21 Feb 2025 09:17:19 -0800 Subject: [PATCH 23/23] chore: alphabetization --- packages/ui/src/helpers/authenticator/facade.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index d5de31403ab..dfed85298c5 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -54,6 +54,7 @@ type AuthenticatorValidationErrors = ValidationError; export type AuthStatus = 'configuring' | 'authenticated' | 'unauthenticated'; interface AuthenticatorServiceContextFacade { + allowedMfaTypes: AuthMFAType[] | undefined; authStatus: AuthStatus; challengeName: ChallengeName | undefined; codeDeliveryDetails: V5CodeDeliveryDetails; @@ -67,7 +68,6 @@ interface AuthenticatorServiceContextFacade { user: AuthUser; username: string; validationErrors: AuthenticatorValidationErrors; - allowedMfaTypes: AuthMFAType[] | undefined; } type SendEventAlias = @@ -93,6 +93,7 @@ export interface AuthenticatorServiceFacade AuthenticatorServiceContextFacade {} interface NextAuthenticatorServiceContextFacade { + allowedMfaTypes: AuthMFAType[] | undefined; challengeName: ChallengeName | undefined; codeDeliveryDetails: V5CodeDeliveryDetails | undefined; errorMessage: string | undefined; @@ -103,7 +104,6 @@ interface NextAuthenticatorServiceContextFacade { totpSecretCode: string | undefined; username: string | undefined; unverifiedUserAttributes: UnverifiedUserAttributes | undefined; - allowedMfaTypes: AuthMFAType[] | undefined; } interface NextAuthenticatorSendEventAliases @@ -249,13 +249,13 @@ export const getNextServiceContextFacade = ( ): NextAuthenticatorServiceContextFacade => { const actorContext = (getActorContext(state) ?? {}) as AuthActorContext; const { + allowedMfaTypes, challengeName, codeDeliveryDetails, remoteError: errorMessage, totpSecretCode, unverifiedUserAttributes, username, - allowedMfaTypes, } = actorContext; const { socialProviders: federatedProviders, loginMechanisms } = @@ -270,6 +270,7 @@ export const getNextServiceContextFacade = ( const route = getRoute(state, actorState) as AuthenticatorRoute; return { + allowedMfaTypes, challengeName, codeDeliveryDetails, errorMessage, @@ -280,7 +281,6 @@ export const getNextServiceContextFacade = ( totpSecretCode, unverifiedUserAttributes, username, - allowedMfaTypes, }; };