Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(authenticator): email mfa #6257

Draft
wants to merge 6 commits into
base: feat-email-mfa/main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -84,6 +91,7 @@ export const DEFAULTS: DefaultComponents<{}> = {
ConfirmVerifyUser,
ForceNewPassword,
ForgotPassword,
SelectMfa,
SetupTotp,
SignIn,
SignUp,
Expand Down
2 changes: 2 additions & 0 deletions packages/react-core/src/Authenticator/hooks/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const COMPONENT_ROUTE_KEYS: AuthenticatorRouteComponentKey[] = [
'confirmVerifyUser',
'forceNewPassword',
'forgotPassword',
'selectMfa',
'setupTotp',
'signIn',
'signUp',
Expand All @@ -23,6 +24,7 @@ export const COMPONENT_ROUTE_NAMES: AuthenticatorRouteComponentName[] = [
'ConfirmVerifyUser',
'ForceNewPassword',
'ForgotPassword',
'SelectMfa',
'SetupTotp',
'SignIn',
'SignUp',
Expand Down
9 changes: 9 additions & 0 deletions packages/react-core/src/Authenticator/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type AuthenticatorRouteComponentKey =
| 'confirmVerifyUser'
| 'forceNewPassword'
| 'forgotPassword'
| 'selectMfa'
| 'setupTotp'
| 'signIn'
| 'signUp'
Expand Down Expand Up @@ -123,6 +124,13 @@ export type ResetPasswordBaseProps<FieldType = {}> = {
ComponentSlots<FieldType> &
ValidationProps;

export type SelectMfaBaseProps<FieldType = {}> = {
challengeName: ChallengeName | undefined;
toSignIn: UseAuthenticator['toSignIn'];
} & CommonRouteProps &
ComponentSlots<FieldType> &
ValidationProps;

export type SetupTotpBaseProps<FieldType = {}> = {
toSignIn: UseAuthenticator['toSignIn'];
totpSecretCode: UseAuthenticator['totpSecretCode'];
Expand Down Expand Up @@ -163,6 +171,7 @@ export interface DefaultProps<FieldType = {}> {
ConfirmVerifyUser: ConfirmVerifyUserProps<FieldType>;
ForceNewPassword: ForceResetPasswordBaseProps<FieldType>;
ForgotPassword: ResetPasswordBaseProps<FieldType>;
SelectMfa: SelectMfaBaseProps<FieldType>;
SetupTotp: SetupTotpBaseProps<FieldType>;
SignIn: SignInBaseProps<FieldType>;
SignUp: SignUpBaseProps<FieldType>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type AuthenticatorRouteComponentKey =
| 'confirmSignUp'
| 'confirmVerifyUser'
| 'forgotPassword'
| 'selectMfa'
| 'setupTotp'
| 'verifyUser';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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':
Expand Down
178 changes: 178 additions & 0 deletions packages/react/src/components/Authenticator/SelectMfa/SelectMfa.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React from 'react';
import { authenticatorTextUtil, MfaType } from '@aws-amplify/ui';

import { Flex } from '../../../primitives/Flex';
import { Heading } from '../../../primitives/Heading';
import { Radio } from '../../../primitives/Radio';
import { RadioGroupField } from '../../../primitives/RadioGroupField';
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';
import { toLower } from 'lodash';

const { getChallengeText } = authenticatorTextUtil;

function parseMfaTypes(types?: MfaType[]): string {
if(types) {
return types.join();
jjarvisp marked this conversation as resolved.
Show resolved Hide resolved
} else {
return 'types was undefined.';
}
}

/*
interface MfaOptions {
email: string;
username: string;
}

const opt: MfaOptions = {
email: 'EMAIL',
username: 'Totp Prop',
};

type MfaType = 'EMAIL' | 'wow';
*/

const generateRadioGroup = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have one of the radio options selected by default?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or make sure the continue button is disabled until one is selected.

attributes?: MfaType[]
): JSX.Element[] => {
const elements: JSX.Element[] = [];
console.log('print attributes: ', attributes);
attributes.forEach((value, index) => {
console.log('print value and index: ', value, index);
elements.push((
<Radio
name="mfa_selection"
value={value}
key={value}
>
Option {index}: {value}
</Radio>
));
});
return elements;
/*
return attributes.map
([key, value]: [string, string], index) => {
const MfaType =
return (
<Radio
name="MfaAtt"
value={key}
key={key}
defaultChecked={index === 0}
>
{MfaType}:{' '}
{value}
</Radio>
);
}
);
*/
};


export const SelectMfa = ({
jordanvn marked this conversation as resolved.
Show resolved Hide resolved
className,
variation,
}: RouteProps): JSX.Element => {
const { isPending } = useAuthenticator((context) => [context.isPending]);
const { allowedMFATypes } = useAuthenticator(({ allowedMFATypes }) => [
allowedMFATypes,
]);

const { handleChange, handleSubmit } = useFormHandlers();

const {
components: {
// @ts-ignore
SelectMfa: {
Header = SelectMfa.Header,
Footer = SelectMfa.Footer,
},
},
} = useCustomComponents();

const verificationRadioGroup = (
<RadioGroupField
legend="What up peeps"
name="figure it out"
isDisabled={isPending}
>
{generateRadioGroup(allowedMFATypes)}
</RadioGroupField>

/*
<RadioGroupField
legend="What up peeps"
name="figure it out"
isDisabled={isPending}
>
<Radio
name="mfa_selection"
value="EMAIL"
key="EMAIL"
>
email:EMAIL
</Radio>
<Radio
name="mfa_selection"
value="TOTP"
key="wow"
>
totp:wow
</Radio>
</RadioGroupField>

*/
);

return (
<RouteContainer className={className} variation={variation}>
<form
data-amplify-form=""
data-amplify-authenticator-select-mfa=""
method="post"
onChange={handleChange}
onSubmit={handleSubmit}
>
<Flex as="fieldset" direction="column" isDisabled={isPending}>
<Header />

<Flex direction="column">
{verificationRadioGroup}

<RemoteErrorMessage />
</Flex>

<ConfirmSignInFooter />
<Footer />
</Flex>
</form>
</RouteContainer>
);
};

SelectMfa.Header = function Header(): JSX.Element {
const { challengeName } = useAuthenticator(({ challengeName }) => [
challengeName,
]);

const { allowedMFATypes } = useAuthenticator(({ allowedMFATypes }) => [
allowedMFATypes,
]);

return <Heading level={3}>{getChallengeText(challengeName)}<br/>MFA types: {parseMfaTypes(allowedMFATypes)}</Heading>;
}

SelectMfa.Footer = function Footer(): JSX.Element {
// @ts-ignore
return null;
};

// <FormFields />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './SelectMfa';
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SetupTotp } from '../../SetupTotp';
import { ConfirmSignIn } from '../../ConfirmSignIn/ConfirmSignIn';
import { ConfirmVerifyUser, VerifyUser } from '../../VerifyUser';
import { ConfirmResetPassword, ForgotPassword } from '../../ForgotPassword';
import { SelectMfa } from '../../SelectMfa';

// use the very generic name of Components as this is a temporary interface and is not exported
interface Components {
Expand All @@ -21,6 +22,7 @@ export interface DefaultComponents extends Omit<Components, 'FormFields'> {
ConfirmVerifyUser?: Omit<Components, 'FormFields'>;
ForceNewPassword?: Components;
ForgotPassword?: Omit<Components, 'FormFields'>;
SelectMfa?: Omit<Components, 'FormFields'>;
SetupTotp?: Omit<Components, 'FormFields'>;
SignIn?: Omit<Components, 'FormFields'>;
SignUp?: Components;
Expand All @@ -43,6 +45,10 @@ export const defaultComponents: DefaultComponents = {
Header: ConfirmSignUp.Header,
Footer: ConfirmSignUp.Footer,
},
SelectMfa: {
Header: SelectMfa.Header,
Footer: SelectMfa.Footer,
},
SetupTotp: {
Header: SetupTotp.Header,
Footer: SetupTotp.Footer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function useFormHandlers(): {
({
target: { checked, name, type, value },
}: React.ChangeEvent<HTMLFormElement>) => {
console.log(checked, name, type, value);
const isUncheckedCheckbox = type === 'checkbox' && !checked;
updateForm({
name,
Expand All @@ -39,7 +40,10 @@ export function useFormHandlers(): {
const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
submitForm(getFormDataFromEvent(event));
const a = getFormDataFromEvent(event);
console.log(a);
submitForm(a);
//submitForm(getFormDataFromEvent(event));
},
[submitForm]
);
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/helpers/authenticator/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export const defaultFormFieldOptions: DefaultFormFieldOptions = {
autocomplete: 'bday',
isRequired: true,
},
mfa_selection: {
label: 'MFA Selection',
type: 'text',
isRequired: true,
},
confirmation_code: {
label: 'Confirmation Code',
placeholder: 'Enter your Confirmation Code',
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/helpers/authenticator/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SocialProvider,
UnverifiedUserAttributes,
ValidationError,
MfaType,
} from '../../types';

import {
Expand Down Expand Up @@ -41,6 +42,7 @@ export type AuthenticatorRoute =
| 'forgotPassword'
| 'setup'
| 'signOut'
| 'selectMfa'
| 'setupTotp'
| 'signIn'
| 'signUp'
Expand All @@ -59,6 +61,7 @@ interface AuthenticatorServiceContextFacade {
isPending: boolean;
route: AuthenticatorRoute;
socialProviders: SocialProvider[];
allowedMFATypes: MfaType[] | undefined;
totpSecretCode: string | null;
unverifiedUserAttributes: UnverifiedUserAttributes;
user: AuthUser;
Expand Down Expand Up @@ -173,6 +176,7 @@ export const getServiceContextFacade = (
const actorContext = (getActorContext(state) ?? {}) as AuthActorContext;
const {
challengeName,
allowedMFATypes,
codeDeliveryDetails,
remoteError: error,
validationError: validationErrors,
Expand Down Expand Up @@ -219,6 +223,7 @@ export const getServiceContextFacade = (
isPending,
route,
socialProviders,
allowedMFATypes,
totpSecretCode,
unverifiedUserAttributes,
user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ const getConfirmationCodeFormFields = (_: AuthMachineState): FormFields => ({
},
});

const getMfaSelectionFormFields = (_: AuthMachineState): FormFields => ({
mfa_selection: {
...getDefaultFormField('mfa_selection'),
label: 'MFA Selection!'
}
});

const getSignInFormFields = (state: AuthMachineState): FormFields => ({
username: { ...getAliasDefaultFormField(state) },
password: {
Expand Down Expand Up @@ -179,5 +186,6 @@ export const defaultFormFieldsGetters: Record<
forgotPassword: getForgotPasswordFormFields,
confirmResetPassword: getConfirmResetPasswordFormFields,
confirmVerifyUser: getConfirmationCodeFormFields,
selectMfa: getMfaSelectionFormFields,
setupTotp: getConfirmationCodeFormFields,
};
Loading