Skip to content

Commit

Permalink
[Email MFA] Updating fetchMFAPreference and updateMFAPreference (#13720)
Browse files Browse the repository at this point in the history
* add EMAIL MFA option in fetchMFAPreference

* add EMAIL MFA option in updateMFAPreference

* update fetchMFAPreference tests

* update updateMFAPreference tests

* update bundle size

* remove redundant assertions
  • Loading branch information
jjarvisp authored Aug 20, 2024
1 parent a8f0747 commit 4ed4918
Showing 8 changed files with 127 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -22,8 +22,8 @@ jest.mock(

describe('fetchMFAPreference', () => {
// assert mocks
const mockFetchAuthSession = fetchAuthSession as jest.Mock;
const mockGetUser = getUser as jest.Mock;
const mockFetchAuthSession = jest.mocked(fetchAuthSession);
const mockGetUser = jest.mocked(getUser);

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

beforeEach(() => {
mockGetUser.mockResolvedValue({
afterEach(() => {
mockGetUser.mockReset();
mockFetchAuthSession.mockClear();
});

it('should return correct MFA preferences when SMS is preferred', async () => {
mockGetUser.mockResolvedValueOnce({
UserAttributes: [],
Username: 'XXXXXXXX',
PreferredMfaSetting: 'SMS_MFA',
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA'],
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
$metadata: {},
});
const resp = await fetchMFAPreference();
expect(resp).toEqual({
preferred: 'SMS',
enabled: ['SMS', 'TOTP', 'EMAIL'],
});
});

afterEach(() => {
mockGetUser.mockReset();
mockFetchAuthSession.mockClear();
it('should return correct MFA preferences when EMAIL is preferred', async () => {
mockGetUser.mockResolvedValueOnce({
UserAttributes: [],
Username: 'XXXXXXXX',
PreferredMfaSetting: 'EMAIL_OTP',
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
$metadata: {},
});
const resp = await fetchMFAPreference();
expect(resp).toEqual({
preferred: 'EMAIL',
enabled: ['SMS', 'TOTP', 'EMAIL'],
});
});

it('should return the preferred MFA setting', async () => {
it('should return correct MFA preferences when TOTP is preferred', async () => {
mockGetUser.mockResolvedValueOnce({
UserAttributes: [],
Username: 'XXXXXXXX',
PreferredMfaSetting: 'SOFTWARE_TOKEN_MFA',
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
$metadata: {},
});
const resp = await fetchMFAPreference();
expect(resp).toEqual({
preferred: 'TOTP',
enabled: ['SMS', 'TOTP', 'EMAIL'],
});
});
it('should return the correct MFA preferences when there is no preferred option', async () => {
mockGetUser.mockResolvedValueOnce({
UserAttributes: [],
Username: 'XXXXXXXX',
UserMFASettingList: ['SMS_MFA', 'SOFTWARE_TOKEN_MFA', 'EMAIL_OTP'],
$metadata: {},
});
const resp = await fetchMFAPreference();
expect(resp).toEqual({
enabled: ['SMS', 'TOTP', 'EMAIL'],
});
});
it('should return the correct MFA preferences when there is no available options', async () => {
mockGetUser.mockResolvedValueOnce({
UserAttributes: [],
Username: 'XXXXXXXX',
$metadata: {},
});
const resp = await fetchMFAPreference();
expect(resp).toEqual({ preferred: 'SMS', enabled: ['SMS', 'TOTP'] });
expect(mockGetUser).toHaveBeenCalledTimes(1);
expect(mockGetUser).toHaveBeenCalledWith(
{
region: 'us-west-2',
userAgentValue: expect.any(String),
},
{
AccessToken: mockAccessToken,
},
);
expect(resp).toEqual({});
});

it('should throw an error when service returns an error response', async () => {
Original file line number Diff line number Diff line change
@@ -12,10 +12,13 @@ import { setUserMFAPreference } from '../../../src/providers/cognito/utils/clien
import { AuthError } from '../../../src/errors/AuthError';
import { SetUserMFAPreferenceException } from '../../../src/providers/cognito/types/errors';
import { getMFASettings } from '../../../src/providers/cognito/apis/updateMFAPreference';
import { MFAPreference } from '../../../src/providers/cognito/types';

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

type MfaPreferenceValue = MFAPreference | undefined;

jest.mock('@aws-amplify/core', () => ({
...(jest.createMockFromModule('@aws-amplify/core') as object),
Amplify: { getConfig: jest.fn(() => ({})) },
@@ -28,25 +31,37 @@ jest.mock(
'../../../src/providers/cognito/utils/clients/CognitoIdentityProvider',
);

const mfaChoices: UpdateMFAPreferenceInput[] = [
{ sms: 'DISABLED', totp: 'DISABLED' },
{ sms: 'DISABLED', totp: 'ENABLED' },
{ sms: 'DISABLED', totp: 'PREFERRED' },
{ sms: 'DISABLED', totp: 'NOT_PREFERRED' },
{ sms: 'ENABLED', totp: 'DISABLED' },
{ sms: 'ENABLED', totp: 'ENABLED' },
{ sms: 'ENABLED', totp: 'PREFERRED' },
{ sms: 'ENABLED', totp: 'NOT_PREFERRED' },
{ sms: 'PREFERRED', totp: 'DISABLED' },
{ sms: 'PREFERRED', totp: 'ENABLED' },
{ sms: 'PREFERRED', totp: 'PREFERRED' },
{ sms: 'PREFERRED', totp: 'NOT_PREFERRED' },
{ sms: 'NOT_PREFERRED', totp: 'DISABLED' },
{ sms: 'NOT_PREFERRED', totp: 'ENABLED' },
{ sms: 'NOT_PREFERRED', totp: 'PREFERRED' },
{ sms: 'NOT_PREFERRED', totp: 'NOT_PREFERRED' },
{ sms: undefined, totp: undefined },
];
// generates all preference permutations
const generateUpdateMFAPreferenceOptions = () => {
const mfaPreferenceTypes: MfaPreferenceValue[] = [
'PREFERRED',
'NOT_PREFERRED',
'ENABLED',
'DISABLED',
undefined,
];
const mfaKeys: (keyof UpdateMFAPreferenceInput)[] = ['email', 'sms', 'totp'];

const generatePermutations = <T>(
keys: string[],
values: T[],
): Record<string, T>[] => {
if (!keys.length) return [{}];

const [curr, ...rest] = keys;
const permutations: Record<string, T>[] = [];

for (const value of values) {
for (const perm of generatePermutations(rest, values)) {
permutations.push({ ...perm, [curr]: value });
}
}

return permutations;
};

return generatePermutations(mfaKeys, mfaPreferenceTypes);
};

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

it.each(mfaChoices)(
'should update with sms $sms and totp $totp',
async mfaChoise => {
const { totp, sms } = mfaChoise;
await updateMFAPreference(mfaChoise);
it.each(generateUpdateMFAPreferenceOptions())(
'should update with email $email, sms $sms, and totp $totp',
async mfaChoice => {
const { totp, sms, email } = mfaChoice;
await updateMFAPreference(mfaChoice);
expect(mockSetUserMFAPreference).toHaveBeenCalledWith(
{
region: 'us-west-2',
@@ -83,6 +98,7 @@ describe('updateMFAPreference', () => {
AccessToken: mockAccessToken,
SMSMfaSettings: getMFASettings(sms),
SoftwareTokenMfaSettings: getMFASettings(totp),
EmailMfaSettings: getMFASettings(email),
},
);
},
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ import { getAuthUserAgentValue } from '../../../utils';
export async function updateMFAPreference(
input: UpdateMFAPreferenceInput,
): Promise<void> {
const { sms, totp } = input;
const { sms, totp, email } = input;
const authConfig = Amplify.getConfig().Auth?.Cognito;
assertTokenProviderConfig(authConfig);
const { tokens } = await fetchAuthSession({ forceRefresh: false });
@@ -40,6 +40,7 @@ export async function updateMFAPreference(
AccessToken: tokens.accessToken.toString(),
SMSMfaSettings: getMFASettings(sms),
SoftwareTokenMfaSettings: getMFASettings(totp),
EmailMfaSettings: getMFASettings(email),
},
);
}
1 change: 1 addition & 0 deletions packages/auth/src/providers/cognito/types/inputs.ts
Original file line number Diff line number Diff line change
@@ -118,6 +118,7 @@ export type SignUpInput = AuthSignUpInput<SignUpOptions<UserAttributeKey>>;
export interface UpdateMFAPreferenceInput {
sms?: MFAPreference;
totp?: MFAPreference;
email?: MFAPreference;
}

/**
Original file line number Diff line number Diff line change
@@ -1430,6 +1430,10 @@ export interface SetUserMFAPreferenceRequest {
* <p>The time-based one-time password software token MFA settings.</p>
*/
SoftwareTokenMfaSettings?: SoftwareTokenMfaSettingsType;
/**
* <p>The email message multi-factor authentication (MFA) settings.</p>
*/
EmailMfaSettings?: EmailMfaSettingsType;
/**
* <p>The access token for the user.</p>
*/
@@ -1538,6 +1542,22 @@ export interface SoftwareTokenMfaSettingsType {
*/
PreferredMfa?: boolean;
}
/**
* <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
* 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
* Authentication for the user pool.</p>
*/
export interface EmailMfaSettingsType {
/**
* <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
* on and the device has been trusted.</p>
*/
Enabled?: boolean;
/**
* <p>Specifies whether email MFA is the preferred MFA method.</p>
*/
PreferredMfa?: boolean;
}
export type UpdateDeviceStatusCommandInput = UpdateDeviceStatusRequest;
export interface UpdateDeviceStatusCommandOutput
extends UpdateDeviceStatusResponse,
1 change: 1 addition & 0 deletions packages/auth/src/providers/cognito/utils/signInHelpers.ts
Original file line number Diff line number Diff line change
@@ -988,6 +988,7 @@ export function mapMfaType(mfa: string): CognitoMFAType {
export function getMFAType(type?: string): AuthMFAType | undefined {
if (type === 'SMS_MFA') return 'SMS';
if (type === 'SOFTWARE_TOKEN_MFA') return 'TOTP';
if (type === 'EMAIL_OTP') return 'EMAIL';
// TODO: log warning for unknown MFA type
}

2 changes: 1 addition & 1 deletion packages/auth/src/types/models.ts
Original file line number Diff line number Diff line change
@@ -44,7 +44,7 @@ export interface AuthTOTPSetupDetails {
getSetupUri(appName: string, accountName?: string): URL;
}

export type AuthMFAType = 'SMS' | 'TOTP';
export type AuthMFAType = 'SMS' | 'TOTP' | 'EMAIL';

export type AuthAllowedMFATypes = AuthMFAType[];

2 changes: 1 addition & 1 deletion packages/aws-amplify/package.json
Original file line number Diff line number Diff line change
@@ -395,7 +395,7 @@
"name": "[Auth] fetchMFAPreference (Cognito)",
"path": "./dist/esm/auth/index.mjs",
"import": "{ fetchMFAPreference }",
"limit": "11.86 kB"
"limit": "11.87 kB"
},
{
"name": "[Auth] verifyTOTPSetup (Cognito)",

0 comments on commit 4ed4918

Please sign in to comment.