diff --git a/packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts b/packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts index ed2517a358e..877059478e3 100644 --- a/packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts +++ b/packages/auth/__tests__/providers/cognito/fetchMFAPreference.test.ts @@ -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 () => { diff --git a/packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts b/packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts index dbaeca398f6..faf14ee04f5 100644 --- a/packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts +++ b/packages/auth/__tests__/providers/cognito/updateMFAPreference.test.ts @@ -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 = ( + keys: string[], + values: T[], + ): Record[] => { + if (!keys.length) return [{}]; + + const [curr, ...rest] = keys; + const permutations: Record[] = []; + + 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), }, ); }, diff --git a/packages/auth/src/providers/cognito/apis/updateMFAPreference.ts b/packages/auth/src/providers/cognito/apis/updateMFAPreference.ts index 790cc82f8bd..807c51ba127 100644 --- a/packages/auth/src/providers/cognito/apis/updateMFAPreference.ts +++ b/packages/auth/src/providers/cognito/apis/updateMFAPreference.ts @@ -26,7 +26,7 @@ import { getAuthUserAgentValue } from '../../../utils'; export async function updateMFAPreference( input: UpdateMFAPreferenceInput, ): Promise { - 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), }, ); } diff --git a/packages/auth/src/providers/cognito/types/inputs.ts b/packages/auth/src/providers/cognito/types/inputs.ts index fa7223f71da..13952bf53e9 100644 --- a/packages/auth/src/providers/cognito/types/inputs.ts +++ b/packages/auth/src/providers/cognito/types/inputs.ts @@ -118,6 +118,7 @@ export type SignUpInput = AuthSignUpInput>; export interface UpdateMFAPreferenceInput { sms?: MFAPreference; totp?: MFAPreference; + email?: MFAPreference; } /** diff --git a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/types.ts b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/types.ts index c08589ad448..571ecf2276c 100644 --- a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/types.ts +++ b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/types.ts @@ -1430,6 +1430,10 @@ export interface SetUserMFAPreferenceRequest { *

The time-based one-time password software token MFA settings.

*/ SoftwareTokenMfaSettings?: SoftwareTokenMfaSettingsType; + /** + *

The email message multi-factor authentication (MFA) settings.

+ */ + EmailMfaSettings?: EmailMfaSettingsType; /** *

The access token for the user.

*/ @@ -1538,6 +1542,22 @@ export interface SoftwareTokenMfaSettingsType { */ PreferredMfa?: boolean; } +/** + *

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.

+ */ +export interface EmailMfaSettingsType { + /** + *

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.

+ */ + Enabled?: boolean; + /** + *

Specifies whether email MFA is the preferred MFA method.

+ */ + PreferredMfa?: boolean; +} export type UpdateDeviceStatusCommandInput = UpdateDeviceStatusRequest; export interface UpdateDeviceStatusCommandOutput extends UpdateDeviceStatusResponse, diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 13edcd84e62..8432fee7b30 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -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 } diff --git a/packages/auth/src/types/models.ts b/packages/auth/src/types/models.ts index 9bcc006141d..64b80fdd874 100644 --- a/packages/auth/src/types/models.ts +++ b/packages/auth/src/types/models.ts @@ -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[]; diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 8508cc83082..69fd71238ee 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -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)",