From ea581db54332478dbedbef3e88e17c1a91273be9 Mon Sep 17 00:00:00 2001 From: Parker Scanlon <69879391+scanlonp@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:56:02 -0800 Subject: [PATCH 1/5] feat(authenticator): add initial email mfa types and confirm sign in with code flow --- .../ui/src/helpers/authenticator/textUtil.ts | 4 ++ .../authenticator/defaultTexts.ts | 2 + .../ui/src/machines/authenticator/actions.ts | 4 ++ .../machines/authenticator/actors/signIn.ts | 60 +++++++++++++++++++ .../ui/src/machines/authenticator/guards.ts | 11 +++- .../ui/src/machines/authenticator/types.ts | 5 ++ 6 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/helpers/authenticator/textUtil.ts b/packages/ui/src/helpers/authenticator/textUtil.ts index 173c1f5f807..8add8d66459 100644 --- a/packages/ui/src/helpers/authenticator/textUtil.ts +++ b/packages/ui/src/helpers/authenticator/textUtil.ts @@ -15,6 +15,10 @@ const getChallengeText = (challengeName?: ChallengeName): string => { return translate(DefaultTexts.CONFIRM_SMS); case 'SOFTWARE_TOKEN_MFA': return translate(DefaultTexts.CONFIRM_TOTP); + case 'EMAIL_OTP': + return translate(DefaultTexts.CONFIRM_EMAIL); + case 'SELECT_MFA_TYPE': + return translate(DefaultTexts.SELECT_MFA_TYPE); default: return translate(DefaultTexts.CONFIRM_MFA_DEFAULT); } diff --git a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts index b040b9ceeda..45dfa6669ae 100644 --- a/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts +++ b/packages/ui/src/i18n/dictionaries/authenticator/defaultTexts.ts @@ -17,6 +17,8 @@ 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', + SELECT_MFA_TYPE: 'Select MFA Type', // If challenge name is not returned CONFIRM_MFA_DEFAULT: 'Confirm MFA Code', CONFIRM_TOTP: 'Confirm TOTP Code', diff --git a/packages/ui/src/machines/authenticator/actions.ts b/packages/ui/src/machines/authenticator/actions.ts index 18985d5109b..e6eae417a31 100644 --- a/packages/ui/src/machines/authenticator/actions.ts +++ b/packages/ui/src/machines/authenticator/actions.ts @@ -63,6 +63,10 @@ const setChallengeName = assign({ ? 'SMS_MFA' : signInStep === 'CONFIRM_SIGN_IN_WITH_TOTP_CODE' ? 'SOFTWARE_TOKEN_MFA' + : signInStep === 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE' + ? 'EMAIL_OTP' + : signInStep === 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION' + ? 'SELECT_MFA_TYPE' : undefined; }, }); diff --git a/packages/ui/src/machines/authenticator/actors/signIn.ts b/packages/ui/src/machines/authenticator/actors/signIn.ts index 297254dafdc..a5f25001c22 100644 --- a/packages/ui/src/machines/authenticator/actors/signIn.ts +++ b/packages/ui/src/machines/authenticator/actors/signIn.ts @@ -90,10 +90,24 @@ export function signInActor({ services }: SignInMachineOptions) { cond: 'shouldConfirmSignIn', target: 'confirmSignIn', }, + /* + { + cond: 'shouldSelectMfa', + target: 'confirmSignIn', + }, + */ + { + cond: 'shouldSelectMfa', + target: 'selectMfa', + }, { cond: 'shouldSetupTotp', target: 'setupTotp', }, + { + cond: 'shouldSetupEmailMfa', + target: 'setupEmailMfa', // implement this + }, { cond: ({ step }) => step === 'CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED', @@ -273,6 +287,52 @@ export function signInActor({ services }: SignInMachineOptions) { }, }, }, + setupEmailMfa: { + 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 }, + }, + }, + }, + selectMfa: { + 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, + }, + }, + }, + }, resolved: { type: 'final', data: (context): ActorDoneData => ({ diff --git a/packages/ui/src/machines/authenticator/guards.ts b/packages/ui/src/machines/authenticator/guards.ts index 7a215bcb130..83ab2a68bc6 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 @@ -55,7 +56,7 @@ const isConfirmUserAttributeStep = (_: AuthActorContext, { data }: AuthEvent) => const isShouldConfirmUserAttributeStep = ( _: AuthActorContext, - { data }: AuthEvent + { data }: AuthEvent ) => data?.step === 'SHOULD_CONFIRM_USER_ATTRIBUTE'; const isResetPasswordStep = (_: AuthActorContext, { data }: AuthEvent) => @@ -80,6 +81,12 @@ const shouldConfirmResetPassword = ({ step }: AuthActorContext) => const shouldConfirmSignUp = ({ step }: AuthActorContext) => step === 'CONFIRM_SIGN_UP'; +const shouldSetupEmailMfa = ({ step }: AuthActorContext) => + step === 'CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP'; // 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP' in js library + +const shouldSelectMfa = ({ step }: AuthActorContext) => + step === 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION'; + // miscellaneous guards const shouldVerifyAttribute = ( _: AuthActorContext, @@ -132,6 +139,8 @@ const GUARDS: MachineOptions['guards'] = { shouldResetPassword, shouldResetPasswordFromSignIn, shouldSetupTotp, + shouldSetupEmailMfa, + shouldSelectMfa, shouldVerifyAttribute, }; diff --git a/packages/ui/src/machines/authenticator/types.ts b/packages/ui/src/machines/authenticator/types.ts index 85da14cf88b..676916ad30c 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' @@ -147,6 +148,10 @@ export type SignInStep = | 'CONFIRM_SIGN_UP' | 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP' | 'RESET_PASSWORD' + | 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION' + | 'CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP' // 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP' in js library + | 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION' + | 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE' | 'SIGN_IN_COMPLETE'; // 'DONE' export type ResetPasswordStep = From eb2bb8b10f74b7874dd22f9c885fa7cc88509617 Mon Sep 17 00:00:00 2001 From: Parker Scanlon <69879391+scanlonp@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:58:31 -0800 Subject: [PATCH 2/5] feat(authenticator): add initial mfa selection react page and ui type helpers --- .../hooks/__mocks__/components.ts | 8 +++ .../src/Authenticator/hooks/constants.ts | 2 + .../src/Authenticator/hooks/types.ts | 9 +++ .../hooks/useAuthenticator/types.ts | 1 + .../Authenticator/Router/Router.tsx | 3 + .../Authenticator/SelectMfa/SelectMfa.tsx | 69 +++++++++++++++++++ .../Authenticator/SelectMfa/index.ts | 1 + .../useCustomComponents/defaultComponents.tsx | 6 ++ .../ui/src/helpers/authenticator/constants.ts | 8 +++ .../ui/src/helpers/authenticator/facade.ts | 1 + .../authenticator/formFields/defaults.ts | 14 ++++ .../ui/src/helpers/authenticator/getRoute.ts | 3 + .../ui/src/types/authenticator/attributes.ts | 2 + packages/ui/src/types/authenticator/form.ts | 3 + 14 files changed, 130 insertions(+) create mode 100644 packages/react/src/components/Authenticator/SelectMfa/SelectMfa.tsx create mode 100644 packages/react/src/components/Authenticator/SelectMfa/index.ts diff --git a/packages/react-core/src/Authenticator/hooks/__mocks__/components.ts b/packages/react-core/src/Authenticator/hooks/__mocks__/components.ts index 9ed530cec11..0e2dae513f7 100644 --- a/packages/react-core/src/Authenticator/hooks/__mocks__/components.ts +++ b/packages/react-core/src/Authenticator/hooks/__mocks__/components.ts @@ -49,6 +49,13 @@ ForgotPassword.Footer = Footer; ForgotPassword.FormFields = FormFields; ForgotPassword.Header = Header; +const SelectMfa: DefaultComponents<{}>['SelectMfa'] = () => { + return null; +}; +SelectMfa.Footer = Footer; +SelectMfa.FormFields = FormFields; +SelectMfa.Header = Header; + const SetupTotp: DefaultComponents<{}>['SetupTotp'] = () => { return null; }; @@ -84,6 +91,7 @@ export const DEFAULTS: DefaultComponents<{}> = { ConfirmVerifyUser, ForceNewPassword, ForgotPassword, + SelectMfa, SetupTotp, SignIn, SignUp, diff --git a/packages/react-core/src/Authenticator/hooks/constants.ts b/packages/react-core/src/Authenticator/hooks/constants.ts index 25dfeb63037..769fdc1f0ff 100644 --- a/packages/react-core/src/Authenticator/hooks/constants.ts +++ b/packages/react-core/src/Authenticator/hooks/constants.ts @@ -10,6 +10,7 @@ export const COMPONENT_ROUTE_KEYS: AuthenticatorRouteComponentKey[] = [ 'confirmVerifyUser', 'forceNewPassword', 'forgotPassword', + 'selectMfa', 'setupTotp', 'signIn', 'signUp', @@ -23,6 +24,7 @@ export const COMPONENT_ROUTE_NAMES: AuthenticatorRouteComponentName[] = [ 'ConfirmVerifyUser', 'ForceNewPassword', 'ForgotPassword', + 'SelectMfa', 'SetupTotp', 'SignIn', 'SignUp', diff --git a/packages/react-core/src/Authenticator/hooks/types.ts b/packages/react-core/src/Authenticator/hooks/types.ts index 2f747ca9c86..c523afde948 100644 --- a/packages/react-core/src/Authenticator/hooks/types.ts +++ b/packages/react-core/src/Authenticator/hooks/types.ts @@ -15,6 +15,7 @@ export type AuthenticatorRouteComponentKey = | 'confirmVerifyUser' | 'forceNewPassword' | 'forgotPassword' + | 'selectMfa' | 'setupTotp' | 'signIn' | 'signUp' @@ -123,6 +124,13 @@ export type ResetPasswordBaseProps = { ComponentSlots & ValidationProps; +export type SelectMfaBaseProps = { + challengeName: ChallengeName | undefined; + toSignIn: UseAuthenticator['toSignIn']; +} & CommonRouteProps & + ComponentSlots & + ValidationProps; + export type SetupTotpBaseProps = { toSignIn: UseAuthenticator['toSignIn']; totpSecretCode: UseAuthenticator['totpSecretCode']; @@ -163,6 +171,7 @@ export interface DefaultProps { ConfirmVerifyUser: ConfirmVerifyUserProps; ForceNewPassword: ForceResetPasswordBaseProps; ForgotPassword: ResetPasswordBaseProps; + SelectMfa: SelectMfaBaseProps; SetupTotp: SetupTotpBaseProps; SignIn: SignInBaseProps; SignUp: SignUpBaseProps; diff --git a/packages/react-core/src/Authenticator/hooks/useAuthenticator/types.ts b/packages/react-core/src/Authenticator/hooks/useAuthenticator/types.ts index 413ab7b2d5b..9ee3553620d 100644 --- a/packages/react-core/src/Authenticator/hooks/useAuthenticator/types.ts +++ b/packages/react-core/src/Authenticator/hooks/useAuthenticator/types.ts @@ -19,6 +19,7 @@ export type AuthenticatorRouteComponentKey = | 'confirmSignUp' | 'confirmVerifyUser' | 'forgotPassword' + | 'selectMfa' | 'setupTotp' | 'verifyUser'; diff --git a/packages/react/src/components/Authenticator/Router/Router.tsx b/packages/react/src/components/Authenticator/Router/Router.tsx index 78c81018af2..c5c4df2915f 100644 --- a/packages/react/src/components/Authenticator/Router/Router.tsx +++ b/packages/react/src/components/Authenticator/Router/Router.tsx @@ -3,6 +3,7 @@ import React, { useMemo } from 'react'; import { useAuthenticator } from '@aws-amplify/ui-react-core'; import { ConfirmSignUp } from '../ConfirmSignUp'; import { ForceNewPassword } from '../ForceNewPassword'; +import { SelectMfa } from '../SelectMfa'; import { SetupTotp } from '../SetupTotp'; import { SignInSignUpTabs } from '../shared'; import { ConfirmVerifyUser, VerifyUser } from '../VerifyUser'; @@ -27,6 +28,8 @@ const getRouteComponent = (route: string): RouteComponent => { return RenderNothing; case 'confirmSignUp': return ConfirmSignUp; + case 'selectMfa': + return SelectMfa; case 'confirmSignIn': return ConfirmSignIn; case 'setupTotp': diff --git a/packages/react/src/components/Authenticator/SelectMfa/SelectMfa.tsx b/packages/react/src/components/Authenticator/SelectMfa/SelectMfa.tsx new file mode 100644 index 00000000000..960d8de8346 --- /dev/null +++ b/packages/react/src/components/Authenticator/SelectMfa/SelectMfa.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { authenticatorTextUtil } from '@aws-amplify/ui'; + +import { Flex } from '../../../primitives/Flex'; +import { Heading } from '../../../primitives/Heading'; +import { useAuthenticator } from '@aws-amplify/ui-react-core'; +import { useCustomComponents } from '../hooks/useCustomComponents'; +import { useFormHandlers } from '../hooks/useFormHandlers'; +import { FormFields } from '../shared/FormFields'; +import { ConfirmSignInFooter } from '../shared/ConfirmSignInFooter'; +import { RemoteErrorMessage } from '../shared/RemoteErrorMessage'; +import { RouteContainer, RouteProps } from '../RouteContainer'; + +const { getChallengeText } = authenticatorTextUtil; + +export const SelectMfa = ({ + className, + variation, +}: RouteProps): JSX.Element => { + const { isPending } = useAuthenticator((context) => [context.isPending]); + const { handleChange, handleSubmit } = useFormHandlers(); + + const { + components: { + // @ts-ignore + SelectMfa: { + Header = SelectMfa.Header, + Footer = SelectMfa.Footer, + }, + }, + } = useCustomComponents(); + + return ( + +
+ +
+ + + + + + + +