Skip to content
Merged
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 @@ -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);
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => ({})) },
Expand All @@ -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
Expand All @@ -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',
Expand All @@ -83,6 +98,7 @@ describe('updateMFAPreference', () => {
AccessToken: mockAccessToken,
SMSMfaSettings: getMFASettings(sms),
SoftwareTokenMfaSettings: getMFASettings(totp),
EmailMfaSettings: getMFASettings(email),
},
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -40,6 +40,7 @@ export async function updateMFAPreference(
AccessToken: tokens.accessToken.toString(),
SMSMfaSettings: getMFASettings(sms),
SoftwareTokenMfaSettings: getMFASettings(totp),
EmailMfaSettings: getMFASettings(email),
},
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/providers/cognito/types/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export type SignUpInput = AuthSignUpInput<SignUpOptions<UserAttributeKey>>;
export interface UpdateMFAPreferenceInput {
sms?: MFAPreference;
totp?: MFAPreference;
email?: MFAPreference;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
*/
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/providers/cognito/utils/signInHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/types/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];

Expand Down
2 changes: 1 addition & 1 deletion packages/aws-amplify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
Loading