Skip to content

Commit 4ed4918

Browse files
authored
[Email MFA] Updating fetchMFAPreference and updateMFAPreference (#13720)
* add EMAIL MFA option in fetchMFAPreference * add EMAIL MFA option in updateMFAPreference * update fetchMFAPreference tests * update updateMFAPreference tests * update bundle size * remove redundant assertions
1 parent a8f0747 commit 4ed4918

File tree

8 files changed

+127
-48
lines changed

8 files changed

+127
-48
lines changed

packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ jest.mock(
2222

2323
describe('fetchMFAPreference', () => {
2424
// assert mocks
25-
const mockFetchAuthSession = fetchAuthSession as jest.Mock;
26-
const mockGetUser = getUser as jest.Mock;
25+
const mockFetchAuthSession = jest.mocked(fetchAuthSession);
26+
const mockGetUser = jest.mocked(getUser);
2727

2828
beforeAll(() => {
2929
setUpGetConfig(Amplify);
@@ -32,34 +32,74 @@ describe('fetchMFAPreference', () => {
3232
});
3333
});
3434

35-
beforeEach(() => {
36-
mockGetUser.mockResolvedValue({
35+
afterEach(() => {
36+
mockGetUser.mockReset();
37+
mockFetchAuthSession.mockClear();
38+
});
39+
40+
it('should return correct MFA preferences when SMS is preferred', async () => {
41+
mockGetUser.mockResolvedValueOnce({
3742
UserAttributes: [],
3843
Username: 'XXXXXXXX',
3944
PreferredMfaSetting: 'SMS_MFA',
40-
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA'],
45+
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
4146
$metadata: {},
4247
});
48+
const resp = await fetchMFAPreference();
49+
expect(resp).toEqual({
50+
preferred: 'SMS',
51+
enabled: ['SMS', 'TOTP', 'EMAIL'],
52+
});
4353
});
4454

45-
afterEach(() => {
46-
mockGetUser.mockReset();
47-
mockFetchAuthSession.mockClear();
55+
it('should return correct MFA preferences when EMAIL is preferred', async () => {
56+
mockGetUser.mockResolvedValueOnce({
57+
UserAttributes: [],
58+
Username: 'XXXXXXXX',
59+
PreferredMfaSetting: 'EMAIL_OTP',
60+
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
61+
$metadata: {},
62+
});
63+
const resp = await fetchMFAPreference();
64+
expect(resp).toEqual({
65+
preferred: 'EMAIL',
66+
enabled: ['SMS', 'TOTP', 'EMAIL'],
67+
});
4868
});
49-
50-
it('should return the preferred MFA setting', async () => {
69+
it('should return correct MFA preferences when TOTP is preferred', async () => {
70+
mockGetUser.mockResolvedValueOnce({
71+
UserAttributes: [],
72+
Username: 'XXXXXXXX',
73+
PreferredMfaSetting: 'SOFTWARE_TOKEN_MFA',
74+
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
75+
$metadata: {},
76+
});
77+
const resp = await fetchMFAPreference();
78+
expect(resp).toEqual({
79+
preferred: 'TOTP',
80+
enabled: ['SMS', 'TOTP', 'EMAIL'],
81+
});
82+
});
83+
it('should return the correct MFA preferences when there is no preferred option', async () => {
84+
mockGetUser.mockResolvedValueOnce({
85+
UserAttributes: [],
86+
Username: 'XXXXXXXX',
87+
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
88+
$metadata: {},
89+
});
90+
const resp = await fetchMFAPreference();
91+
expect(resp).toEqual({
92+
enabled: ['SMS', 'TOTP', 'EMAIL'],
93+
});
94+
});
95+
it('should return the correct MFA preferences when there is no available options', async () => {
96+
mockGetUser.mockResolvedValueOnce({
97+
UserAttributes: [],
98+
Username: 'XXXXXXXX',
99+
$metadata: {},
100+
});
51101
const resp = await fetchMFAPreference();
52-
expect(resp).toEqual({ preferred: 'SMS', enabled: ['SMS', 'TOTP'] });
53-
expect(mockGetUser).toHaveBeenCalledTimes(1);
54-
expect(mockGetUser).toHaveBeenCalledWith(
55-
{
56-
region: 'us-west-2',
57-
userAgentValue: expect.any(String),
58-
},
59-
{
60-
AccessToken: mockAccessToken,
61-
},
62-
);
102+
expect(resp).toEqual({});
63103
});
64104

65105
it('should throw an error when service returns an error response', async () => {

packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import { setUserMFAPreference } from '../../../src/providers/cognito/utils/clien
1212
import { AuthError } from '../../../src/errors/AuthError';
1313
import { SetUserMFAPreferenceException } from '../../../src/providers/cognito/types/errors';
1414
import { getMFASettings } from '../../../src/providers/cognito/apis/updateMFAPreference';
15+
import { MFAPreference } from '../../../src/providers/cognito/types';
1516

1617
import { getMockError, mockAccessToken } from './testUtils/data';
1718
import { setUpGetConfig } from './testUtils/setUpGetConfig';
1819

20+
type MfaPreferenceValue = MFAPreference | undefined;
21+
1922
jest.mock('@aws-amplify/core', () => ({
2023
...(jest.createMockFromModule('@aws-amplify/core') as object),
2124
Amplify: { getConfig: jest.fn(() => ({})) },
@@ -28,25 +31,37 @@ jest.mock(
2831
'../../../src/providers/cognito/utils/clients/CognitoIdentityProvider',
2932
);
3033

31-
const mfaChoices: UpdateMFAPreferenceInput[] = [
32-
{ sms: 'DISABLED', totp: 'DISABLED' },
33-
{ sms: 'DISABLED', totp: 'ENABLED' },
34-
{ sms: 'DISABLED', totp: 'PREFERRED' },
35-
{ sms: 'DISABLED', totp: 'NOT_PREFERRED' },
36-
{ sms: 'ENABLED', totp: 'DISABLED' },
37-
{ sms: 'ENABLED', totp: 'ENABLED' },
38-
{ sms: 'ENABLED', totp: 'PREFERRED' },
39-
{ sms: 'ENABLED', totp: 'NOT_PREFERRED' },
40-
{ sms: 'PREFERRED', totp: 'DISABLED' },
41-
{ sms: 'PREFERRED', totp: 'ENABLED' },
42-
{ sms: 'PREFERRED', totp: 'PREFERRED' },
43-
{ sms: 'PREFERRED', totp: 'NOT_PREFERRED' },
44-
{ sms: 'NOT_PREFERRED', totp: 'DISABLED' },
45-
{ sms: 'NOT_PREFERRED', totp: 'ENABLED' },
46-
{ sms: 'NOT_PREFERRED', totp: 'PREFERRED' },
47-
{ sms: 'NOT_PREFERRED', totp: 'NOT_PREFERRED' },
48-
{ sms: undefined, totp: undefined },
49-
];
34+
// generates all preference permutations
35+
const generateUpdateMFAPreferenceOptions = () => {
36+
const mfaPreferenceTypes: MfaPreferenceValue[] = [
37+
'PREFERRED',
38+
'NOT_PREFERRED',
39+
'ENABLED',
40+
'DISABLED',
41+
undefined,
42+
];
43+
const mfaKeys: (keyof UpdateMFAPreferenceInput)[] = ['email', 'sms', 'totp'];
44+
45+
const generatePermutations = <T>(
46+
keys: string[],
47+
values: T[],
48+
): Record<string, T>[] => {
49+
if (!keys.length) return [{}];
50+
51+
const [curr, ...rest] = keys;
52+
const permutations: Record<string, T>[] = [];
53+
54+
for (const value of values) {
55+
for (const perm of generatePermutations(rest, values)) {
56+
permutations.push({ ...perm, [curr]: value });
57+
}
58+
}
59+
60+
return permutations;
61+
};
62+
63+
return generatePermutations(mfaKeys, mfaPreferenceTypes);
64+
};
5065

5166
describe('updateMFAPreference', () => {
5267
// assert mocks
@@ -69,11 +84,11 @@ describe('updateMFAPreference', () => {
6984
mockFetchAuthSession.mockClear();
7085
});
7186

72-
it.each(mfaChoices)(
73-
'should update with sms $sms and totp $totp',
74-
async mfaChoise => {
75-
const { totp, sms } = mfaChoise;
76-
await updateMFAPreference(mfaChoise);
87+
it.each(generateUpdateMFAPreferenceOptions())(
88+
'should update with email $email, sms $sms, and totp $totp',
89+
async mfaChoice => {
90+
const { totp, sms, email } = mfaChoice;
91+
await updateMFAPreference(mfaChoice);
7792
expect(mockSetUserMFAPreference).toHaveBeenCalledWith(
7893
{
7994
region: 'us-west-2',
@@ -83,6 +98,7 @@ describe('updateMFAPreference', () => {
8398
AccessToken: mockAccessToken,
8499
SMSMfaSettings: getMFASettings(sms),
85100
SoftwareTokenMfaSettings: getMFASettings(totp),
101+
EmailMfaSettings: getMFASettings(email),
86102
},
87103
);
88104
},

packages/auth/src/providers/cognito/apis/updateMFAPreference.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { getAuthUserAgentValue } from '../../../utils';
2626
export async function updateMFAPreference(
2727
input: UpdateMFAPreferenceInput,
2828
): Promise<void> {
29-
const { sms, totp } = input;
29+
const { sms, totp, email } = input;
3030
const authConfig = Amplify.getConfig().Auth?.Cognito;
3131
assertTokenProviderConfig(authConfig);
3232
const { tokens } = await fetchAuthSession({ forceRefresh: false });
@@ -40,6 +40,7 @@ export async function updateMFAPreference(
4040
AccessToken: tokens.accessToken.toString(),
4141
SMSMfaSettings: getMFASettings(sms),
4242
SoftwareTokenMfaSettings: getMFASettings(totp),
43+
EmailMfaSettings: getMFASettings(email),
4344
},
4445
);
4546
}

packages/auth/src/providers/cognito/types/inputs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export type SignUpInput = AuthSignUpInput<SignUpOptions<UserAttributeKey>>;
118118
export interface UpdateMFAPreferenceInput {
119119
sms?: MFAPreference;
120120
totp?: MFAPreference;
121+
email?: MFAPreference;
121122
}
122123

123124
/**

packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,10 @@ export interface SetUserMFAPreferenceRequest {
14301430
* <p>The time-based one-time password software token MFA settings.</p>
14311431
*/
14321432
SoftwareTokenMfaSettings?: SoftwareTokenMfaSettingsType;
1433+
/**
1434+
* <p>The email message multi-factor authentication (MFA) settings.</p>
1435+
*/
1436+
EmailMfaSettings?: EmailMfaSettingsType;
14331437
/**
14341438
* <p>The access token for the user.</p>
14351439
*/
@@ -1538,6 +1542,22 @@ export interface SoftwareTokenMfaSettingsType {
15381542
*/
15391543
PreferredMfa?: boolean;
15401544
}
1545+
/**
1546+
* <p>The type used for enabling email MFA at the user level. If an MFA type is activated for a user, the user will be prompted for MFA during all sign-in attempts, unless device tracking
1547+
* is turned on and the device has been trusted. If you want MFA to be applied selectively based on the assessed risk level of sign-in attempts, deactivate MFA for users and turn on Adaptive
1548+
* Authentication for the user pool.</p>
1549+
*/
1550+
export interface EmailMfaSettingsType {
1551+
/**
1552+
* <p>Specifies whether email MFA is activated. If an MFA type is activated for a user, the user will be prompted for MFA during all sign-in attempts, unless device tracking is turned
1553+
* on and the device has been trusted.</p>
1554+
*/
1555+
Enabled?: boolean;
1556+
/**
1557+
* <p>Specifies whether email MFA is the preferred MFA method.</p>
1558+
*/
1559+
PreferredMfa?: boolean;
1560+
}
15411561
export type UpdateDeviceStatusCommandInput = UpdateDeviceStatusRequest;
15421562
export interface UpdateDeviceStatusCommandOutput
15431563
extends UpdateDeviceStatusResponse,

packages/auth/src/providers/cognito/utils/signInHelpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,7 @@ export function mapMfaType(mfa: string): CognitoMFAType {
988988
export function getMFAType(type?: string): AuthMFAType | undefined {
989989
if (type === 'SMS_MFA') return 'SMS';
990990
if (type === 'SOFTWARE_TOKEN_MFA') return 'TOTP';
991+
if (type === 'EMAIL_OTP') return 'EMAIL';
991992
// TODO: log warning for unknown MFA type
992993
}
993994

packages/auth/src/types/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export interface AuthTOTPSetupDetails {
4444
getSetupUri(appName: string, accountName?: string): URL;
4545
}
4646

47-
export type AuthMFAType = 'SMS' | 'TOTP';
47+
export type AuthMFAType = 'SMS' | 'TOTP' | 'EMAIL';
4848

4949
export type AuthAllowedMFATypes = AuthMFAType[];
5050

packages/aws-amplify/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@
395395
"name": "[Auth] fetchMFAPreference (Cognito)",
396396
"path": "./dist/esm/auth/index.mjs",
397397
"import": "{ fetchMFAPreference }",
398-
"limit": "11.86 kB"
398+
"limit": "11.87 kB"
399399
},
400400
{
401401
"name": "[Auth] verifyTOTPSetup (Cognito)",

0 commit comments

Comments
 (0)