From 164e9cc92a7ec4dbca69d197adf01e607ed0bc18 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 20 Aug 2024 15:26:55 -0400 Subject: [PATCH 01/11] [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 --- .../cognito/fetchMFAPreference.test.ts | 82 ++++++++++++++----- .../cognito/updateMFAPreference.test.ts | 64 +++++++++------ .../cognito/apis/updateMFAPreference.ts | 3 +- .../src/providers/cognito/types/inputs.ts | 1 + .../clients/CognitoIdentityProvider/types.ts | 20 +++++ .../providers/cognito/utils/signInHelpers.ts | 1 + packages/auth/src/types/models.ts | 2 +- 7 files changed, 126 insertions(+), 47 deletions(-) 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[]; From 4b2c4580ce7bc5e1f63591cd80781020f6a0208c Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 27 Aug 2024 11:31:52 -0400 Subject: [PATCH 02/11] [Email MFA] Add support for EMAIL_OTP during sign in flows (#13745) * Confirm Sign In With Email OTP * Confirm Sign In Tests With Email OTP * Update packages/auth/src/types/models.ts Co-authored-by: israx <70438514+israx@users.noreply.github.com> * Fix Errant Pascal Casing --------- Co-authored-by: israx <70438514+israx@users.noreply.github.com> --- .../cognito/confirmSignInErrorCases.test.ts | 2 +- .../cognito/confirmSignInHappyCases.test.ts | 170 +++++++++++++++++- packages/auth/src/common/AuthErrorStrings.ts | 6 +- .../clients/CognitoIdentityProvider/types.ts | 4 +- .../providers/cognito/utils/signInHelpers.ts | 155 ++++++++-------- packages/auth/src/types/models.ts | 16 ++ 6 files changed, 266 insertions(+), 87 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts index 0f20b1703f3..355f89c2ac8 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts @@ -51,7 +51,7 @@ describe('confirmSignIn API error path cases:', () => { } }); - it('should throw an error when sign-in step is CONTINUE_SIGN_IN_WITH_MFA_SELECTION and challengeResponse is not "SMS" or "TOTP"', async () => { + it('should throw an error when sign-in step is CONTINUE_SIGN_IN_WITH_MFA_SELECTION and challengeResponse is not "SMS", "TOTP", or "EMAIL"', async () => { expect.assertions(2); try { await confirmSignIn({ challengeResponse: 'NO_SMS' }); diff --git a/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts index ddeb3c368fd..4e094c3bd0f 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts @@ -122,6 +122,55 @@ describe('confirmSignIn API happy path cases', () => { mockedGetCurrentUser.mockClear(); }); + test(`confirmSignIn with EMAIL_OTP ChallengeName`, async () => { + Amplify.configure({ + Auth: authConfig, + }); + + const handleUserSRPAuthflowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'EMAIL_OTP', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL', + CODE_DELIVERY_DESTINATION: 'j***@a***', + }, + }), + ); + + const signInResult = await signIn({ username, password }); + + expect(signInResult).toEqual({ + isSignedIn: false, + nextStep: { + signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', + codeDeliveryDetails: { + deliveryMedium: 'EMAIL', + destination: 'j***@a***', + }, + }, + }); + + const confirmSignInResult = await confirmSignIn({ + challengeResponse: '123456', + }); + + expect(confirmSignInResult).toEqual({ + isSignedIn: true, + nextStep: { + signInStep: 'DONE', + }, + }); + + expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1); + expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1); + + handleUserSRPAuthflowSpy.mockClear(); + }); + test(`confirmSignIn tests MFA_SETUP challengeName`, async () => { Amplify.configure({ Auth: authConfig, @@ -162,7 +211,7 @@ describe('confirmSignIn API happy path cases', () => { handleUserSRPAuthflowSpy.mockClear(); }); - test(`confirmSignIn tests SELECT_MFA_TYPE challengeName `, async () => { + test(`confirmSignIn with SELECT_MFA_TYPE challengeName and SMS response`, async () => { Amplify.configure({ Auth: authConfig, }); @@ -175,7 +224,7 @@ describe('confirmSignIn API happy path cases', () => { Session: '1234234232', $metadata: {}, ChallengeParameters: { - MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA"]', + MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]', }, }), ); @@ -204,7 +253,7 @@ describe('confirmSignIn API happy path cases', () => { isSignedIn: false, nextStep: { signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION', - allowedMFATypes: ['SMS', 'TOTP'], + allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'], }, }); @@ -226,6 +275,121 @@ describe('confirmSignIn API happy path cases', () => { handleUserSRPAuthflowSpy.mockClear(); }); + test(`confirmSignIn with SELECT_MFA_TYPE challengeName and TOTP response`, async () => { + Amplify.configure({ + Auth: authConfig, + }); + + const handleUserSRPAuthflowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'SELECT_MFA_TYPE', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]', + }, + }), + ); + + handleChallengeNameSpy.mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'SOFTWARE_TOKEN_MFA', + $metadata: {}, + Session: '123456789', + ChallengeParameters: {}, + }), + ); + + const signInResult = await signIn({ username, password }); + + expect(signInResult).toEqual({ + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION', + allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'], + }, + }); + + const confirmSignInResult = await confirmSignIn({ + challengeResponse: 'TOTP', + }); + + expect(confirmSignInResult).toEqual({ + isSignedIn: false, + nextStep: { + signInStep: 'CONFIRM_SIGN_IN_WITH_TOTP_CODE', + }, + }); + + expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1); + expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1); + + handleUserSRPAuthflowSpy.mockClear(); + }); + + test(`confirmSignIn with SELECT_MFA_TYPE challengeName and EMAIL response`, async () => { + Amplify.configure({ + Auth: authConfig, + }); + + const handleUserSRPAuthflowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'SELECT_MFA_TYPE', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + MFAS_CAN_CHOOSE: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]', + }, + }), + ); + + handleChallengeNameSpy.mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'EMAIL_OTP', + $metadata: {}, + Session: '1234234232', + ChallengeParameters: { + CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL', + CODE_DELIVERY_DESTINATION: 'j***@a***', + }, + }), + ); + + const signInResult = await signIn({ username, password }); + + expect(signInResult).toEqual({ + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SELECTION', + allowedMFATypes: ['SMS', 'TOTP', 'EMAIL'], + }, + }); + + const confirmSignInResult = await confirmSignIn({ + challengeResponse: 'EMAIL', + }); + + expect(confirmSignInResult).toEqual({ + isSignedIn: false, + nextStep: { + signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', + codeDeliveryDetails: { + deliveryMedium: 'EMAIL', + destination: 'j***@a***', + }, + }, + }); + + expect(handleChallengeNameSpy).toHaveBeenCalledTimes(1); + expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1); + + handleUserSRPAuthflowSpy.mockClear(); + }); + test('handleChallengeName should be called with clientMetadata and usersub', async () => { Amplify.configure({ Auth: authConfig, diff --git a/packages/auth/src/common/AuthErrorStrings.ts b/packages/auth/src/common/AuthErrorStrings.ts index c05e4d7bf4c..ad4b8c261ef 100644 --- a/packages/auth/src/common/AuthErrorStrings.ts +++ b/packages/auth/src/common/AuthErrorStrings.ts @@ -47,8 +47,10 @@ export const validationErrorMap: AmplifyErrorMap = { recoverySuggestion: 'Do not include a password in your signIn call.', }, [AuthValidationErrorCode.IncorrectMFAMethod]: { - message: 'Incorrect MFA method was chosen. It should be either SMS or TOTP', - recoverySuggestion: 'Try to pass TOTP or SMS as the challengeResponse', + message: + 'Incorrect MFA method was chosen. It should be either SMS, TOTP, or EMAIL', + recoverySuggestion: + 'Try to pass SMS, TOTP, or EMAIL as the challengeResponse', }, [AuthValidationErrorCode.EmptyVerifyTOTPSetupCode]: { message: 'code is required to verifyTotpSetup', 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 571ecf2276c..f7a1d4a483a 100644 --- a/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/types.ts +++ b/packages/auth/src/providers/cognito/utils/clients/CognitoIdentityProvider/types.ts @@ -8,6 +8,7 @@ import { MetadataBearer as __MetadataBearer } from '@aws-sdk/types'; export type ChallengeName = | 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' + | 'EMAIL_OTP' | 'SELECT_MFA_TYPE' | 'MFA_SETUP' | 'PASSWORD_VERIFIER' @@ -28,7 +29,7 @@ export type ChallengeParameters = { MFAS_CAN_SETUP?: string; } & Record; -export type CognitoMFAType = 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA'; +export type CognitoMFAType = 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' | 'EMAIL_OTP'; export interface CognitoMFASettings { Enabled?: boolean; @@ -55,6 +56,7 @@ declare enum ChallengeNameType { SELECT_MFA_TYPE = 'SELECT_MFA_TYPE', SMS_MFA = 'SMS_MFA', SOFTWARE_TOKEN_MFA = 'SOFTWARE_TOKEN_MFA', + EMAIL_OTP = 'EMAIL_OTP', } declare enum DeliveryMediumType { EMAIL = 'EMAIL', diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 8432fee7b30..99a615644b5 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -189,7 +189,9 @@ export async function handleSelectMFATypeChallenge({ }: HandleAuthChallengeRequest): Promise { const { userPoolId, userPoolClientId } = config; assertValidationError( - challengeResponse === 'TOTP' || challengeResponse === 'SMS', + challengeResponse === 'TOTP' || + challengeResponse === 'SMS' || + challengeResponse === 'EMAIL', AuthValidationErrorCode.IncorrectMFAMethod, ); @@ -222,76 +224,6 @@ export async function handleSelectMFATypeChallenge({ ); } -export async function handleSMSMFAChallenge({ - challengeResponse, - clientMetadata, - session, - username, - config, -}: HandleAuthChallengeRequest): Promise { - const { userPoolId, userPoolClientId } = config; - const challengeResponses = { - USERNAME: username, - SMS_MFA_CODE: challengeResponse, - }; - const UserContextData = getUserContextData({ - username, - userPoolId, - userPoolClientId, - }); - const jsonReq: RespondToAuthChallengeCommandInput = { - ChallengeName: 'SMS_MFA', - ChallengeResponses: challengeResponses, - Session: session, - ClientMetadata: clientMetadata, - ClientId: userPoolClientId, - UserContextData, - }; - - return respondToAuthChallenge( - { - region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), - }, - jsonReq, - ); -} -export async function handleSoftwareTokenMFAChallenge({ - challengeResponse, - clientMetadata, - session, - username, - config, -}: HandleAuthChallengeRequest): Promise { - const { userPoolId, userPoolClientId } = config; - const challengeResponses = { - USERNAME: username, - SOFTWARE_TOKEN_MFA_CODE: challengeResponse, - }; - - const UserContextData = getUserContextData({ - username, - userPoolId, - userPoolClientId, - }); - - const jsonReq: RespondToAuthChallengeCommandInput = { - ChallengeName: 'SOFTWARE_TOKEN_MFA', - ChallengeResponses: challengeResponses, - Session: session, - ClientMetadata: clientMetadata, - ClientId: userPoolClientId, - UserContextData, - }; - - return respondToAuthChallenge( - { - region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), - }, - jsonReq, - ); -} export async function handleCompleteNewPasswordChallenge({ challengeResponse, clientMetadata, @@ -824,6 +756,18 @@ export async function getSignInResult(params: { signInStep: 'CONFIRM_SIGN_IN_WITH_TOTP_CODE', }, }; + case 'EMAIL_OTP': + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', + codeDeliveryDetails: { + deliveryMedium: + challengeParameters.CODE_DELIVERY_DELIVERY_MEDIUM as AuthDeliveryMedium, + destination: challengeParameters.CODE_DELIVERY_DESTINATION, + }, + }, + }; case 'ADMIN_NO_SRP_AUTH': break; case 'DEVICE_PASSWORD_VERIFIER': @@ -911,14 +855,6 @@ export async function handleChallengeName( const deviceName = options?.friendlyDeviceName; switch (challengeName) { - case 'SMS_MFA': - return handleSMSMFAChallenge({ - challengeResponse, - clientMetadata, - session, - username, - config, - }); case 'SELECT_MFA_TYPE': return handleSelectMFATypeChallenge({ challengeResponse, @@ -961,8 +897,11 @@ export async function handleChallengeName( username, tokenOrchestrator, ); + case 'SMS_MFA': case 'SOFTWARE_TOKEN_MFA': - return handleSoftwareTokenMFAChallenge({ + case 'EMAIL_OTP': + return handleMFAChallenge({ + challengeName, challengeResponse, clientMetadata, session, @@ -981,6 +920,7 @@ export async function handleChallengeName( export function mapMfaType(mfa: string): CognitoMFAType { let mfaType: CognitoMFAType = 'SMS_MFA'; if (mfa === 'TOTP') mfaType = 'SOFTWARE_TOKEN_MFA'; + if (mfa === 'EMAIL') mfaType = 'EMAIL_OTP'; return mfaType; } @@ -1131,3 +1071,58 @@ export function getActiveSignInUsername(username: string): string { return state.username ?? username; } + +export async function handleMFAChallenge({ + challengeName, + challengeResponse, + clientMetadata, + session, + username, + config, +}: HandleAuthChallengeRequest & { + challengeName: Extract< + ChallengeName, + 'EMAIL_OTP' | 'SMS_MFA' | 'SOFTWARE_TOKEN_MFA' + >; +}) { + const { userPoolId, userPoolClientId } = config; + + const challengeResponses: Record = { + USERNAME: username, + }; + + if (challengeName === 'EMAIL_OTP') { + challengeResponses.EMAIL_OTP_CODE = challengeResponse; + } + + if (challengeName === 'SMS_MFA') { + challengeResponses.SMS_MFA_CODE = challengeResponse; + } + + if (challengeName === 'SOFTWARE_TOKEN_MFA') { + challengeResponses.SOFTWARE_TOKEN_MFA_CODE = challengeResponse; + } + + const userContextData = getUserContextData({ + username, + userPoolId, + userPoolClientId, + }); + + const jsonReq: RespondToAuthChallengeCommandInput = { + ChallengeName: challengeName, + ChallengeResponses: challengeResponses, + Session: session, + ClientMetadata: clientMetadata, + ClientId: userPoolClientId, + UserContextData: userContextData, + }; + + return respondToAuthChallenge( + { + region: getRegion(userPoolId), + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, + jsonReq, + ); +} diff --git a/packages/auth/src/types/models.ts b/packages/auth/src/types/models.ts index 64b80fdd874..0e671266524 100644 --- a/packages/auth/src/types/models.ts +++ b/packages/auth/src/types/models.ts @@ -146,6 +146,21 @@ export interface ConfirmSignInWithSMSCode { codeDeliveryDetails?: AuthCodeDeliveryDetails; } +export interface ConfirmSignInWithEmailCode { + /** + * Auth step requires user to use EMAIL as multifactor authentication by retrieving a code sent to inbox. + * + * @example + * ```typescript + * // Code retrieved from email + * const emailCode = '112233' + * await confirmSignIn({challengeResponse: emailCode}) + * ``` + */ + signInStep: 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE'; + codeDeliveryDetails?: AuthCodeDeliveryDetails; +} + export interface ConfirmSignUpStep { /** * Auth step requires to confirm user's sign-up. @@ -181,6 +196,7 @@ export type AuthNextSignInStep< | ConfirmSignInWithNewPasswordRequired | ConfirmSignInWithSMSCode | ConfirmSignInWithTOTPCode + | ConfirmSignInWithEmailCode | ContinueSignInWithTOTPSetup | ConfirmSignUpStep | ResetPasswordStep From c1c7a5e0eca16032edf1da138905699c72e45666 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Fri, 6 Sep 2024 15:26:00 -0400 Subject: [PATCH 03/11] feat(auth): [EMAIL MFA] Sign In / Confirm Sign In With MFA_SETUP (#13760) * Sign In / Confirm Sign In With MFA_SETUP * Sign In State Management Tests * Confirm Sign In Happy Path Tests * Fix State Management Tests * Apply Feedback * loose email matching * Remove Unnecessary Export * Update SignInHelpers For Getting Allowed MFA Setup Types * Add Error Case Unit Tests --- .../cognito/confirmSignInErrorCases.test.ts | 24 +- .../cognito/confirmSignInHappyCases.test.ts | 244 +++++++++++++++++- .../cognito/signInErrorCases.test.ts | 26 ++ .../cognito/testUtils/authApiTestParams.ts | 25 +- .../providers/cognito/apis/confirmSignIn.ts | 4 +- .../providers/cognito/utils/signInHelpers.ts | 188 ++++++++++---- packages/auth/src/types/models.ts | 31 +++ 7 files changed, 479 insertions(+), 63 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts index 355f89c2ac8..3516c146e6a 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignInErrorCases.test.ts @@ -6,6 +6,7 @@ import { confirmSignIn } from '../../../src/providers/cognito/apis/confirmSignIn import { RespondToAuthChallengeException } from '../../../src/providers/cognito/types/errors'; import { respondToAuthChallenge } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; import { signInStore } from '../../../src/providers/cognito/utils/signInStore'; +import { AuthErrorCodes } from '../../../src/common/AuthErrorStrings'; import { getMockError } from './testUtils/data'; import { setUpGetConfig } from './testUtils/setUpGetConfig'; @@ -25,8 +26,8 @@ describe('confirmSignIn API error path cases:', () => { const signInSession = '1234234232'; const { username } = authAPITestParams.user1; // assert mocks - const mockStoreGetState = signInStore.getState as jest.Mock; - const mockRespondToAuthChallenge = respondToAuthChallenge as jest.Mock; + const mockStoreGetState = jest.mocked(signInStore.getState); + const mockRespondToAuthChallenge = jest.mocked(respondToAuthChallenge); beforeAll(() => { setUpGetConfig(Amplify); @@ -77,4 +78,23 @@ describe('confirmSignIn API error path cases:', () => { ); } }); + it('should throw an error when sign-in step is MFA_SETUP and challengeResponse is not valid', async () => { + expect.assertions(3); + + mockStoreGetState.mockReturnValue({ + username, + challengeName: 'MFA_SETUP', + signInSession, + }); + + try { + await confirmSignIn({ + challengeResponse: 'SMS', + }); + } catch (err: any) { + expect(err).toBeInstanceOf(AuthError); + expect(err.name).toBe(AuthErrorCodes.SignInException); + expect(err.message).toContain('SMS'); + } + }); }); diff --git a/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts b/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts index 4e094c3bd0f..11bd3a85b08 100644 --- a/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/confirmSignInHappyCases.test.ts @@ -29,7 +29,7 @@ const authConfig = { // getCurrentUser is mocked so Hub is able to dispatch a mocked AuthUser // before returning an `AuthSignInResult` -const mockedGetCurrentUser = getCurrentUser as jest.Mock; +const mockedGetCurrentUser = jest.mocked(getCurrentUser); describe('confirmSignIn API happy path cases', () => { let handleChallengeNameSpy: jest.SpyInstance; @@ -706,3 +706,245 @@ describe('Cognito ASF', () => { ); }); }); + +describe('confirmSignIn MFA_SETUP challenge happy path cases', () => { + const { username, password } = authAPITestParams.user1; + + test('confirmSignIn with multiple MFA_SETUP options using SOFTWARE_TOKEN_MFA', async () => { + Amplify.configure({ + Auth: authConfig, + }); + jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeMultipleMfaSetupOutput, + ); + + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe( + 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION', + ); + + jest.spyOn(clients, 'associateSoftwareToken').mockResolvedValueOnce({ + SecretCode: 'secret-code', + Session: '12341234', + $metadata: {}, + }); + + const selectMfaToSetupConfirmSignInResult = await confirmSignIn({ + challengeResponse: 'TOTP', + }); + + expect(selectMfaToSetupConfirmSignInResult.isSignedIn).toBe(false); + expect(selectMfaToSetupConfirmSignInResult.nextStep.signInStep).toBe( + 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP', + ); + + const verifySoftwareTokenSpy = jest + .spyOn(clients, 'verifySoftwareToken') + .mockResolvedValueOnce({ + Session: '12341234', + Status: 'SUCCESS', + $metadata: {}, + }); + + jest + .spyOn(clients, 'respondToAuthChallenge') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeCommandOutput, + ); + + const totpCode = '123456'; + const confirmSignInResult = await confirmSignIn({ + challengeResponse: totpCode, + }); + + expect(verifySoftwareTokenSpy).toHaveBeenCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + UserCode: totpCode, + Session: '12341234', + }), + ); + expect(confirmSignInResult.isSignedIn).toBe(true); + expect(confirmSignInResult.nextStep.signInStep).toBe('DONE'); + }); + + test('confirmSignIn with multiple MFA_SETUP options using EMAIL_OTP', async () => { + Amplify.configure({ + Auth: authConfig, + }); + + jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeMultipleMfaSetupOutput, + ); + + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe( + 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION', + ); + + const selectMfaToSetupConfirmSignInResult = await confirmSignIn({ + challengeResponse: 'EMAIL', + }); + + expect(selectMfaToSetupConfirmSignInResult.isSignedIn).toBe(false); + expect(selectMfaToSetupConfirmSignInResult.nextStep.signInStep).toBe( + 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP', + ); + + jest.spyOn(signInHelpers, 'handleChallengeName').mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'EMAIL_OTP', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL', + CODE_DELIVERY_DESTINATION: 'j***@a***', + }, + }), + ); + + const setupEmailConfirmSignInResult = await confirmSignIn({ + challengeResponse: 'j***@a***', + }); + + expect(setupEmailConfirmSignInResult.nextStep.signInStep).toBe( + 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', + ); + + jest + .spyOn(clients, 'respondToAuthChallenge') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeCommandOutput, + ); + + const confirmSignInResult = await confirmSignIn({ + challengeResponse: '123456', + }); + + expect(confirmSignInResult.isSignedIn).toBe(true); + expect(confirmSignInResult.nextStep.signInStep).toBe('DONE'); + }); + + test('confirmSignIn with single MFA_SETUP option using EMAIL_OTP', async () => { + Amplify.configure({ + Auth: authConfig, + }); + + jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeEmailMfaSetupOutput, + ); + + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe( + 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP', + ); + + jest.spyOn(signInHelpers, 'handleChallengeName').mockImplementationOnce( + async (): Promise => ({ + ChallengeName: 'EMAIL_OTP', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL', + CODE_DELIVERY_DESTINATION: 'j***@a***', + }, + }), + ); + + const setupEmailConfirmSignInResult = await confirmSignIn({ + challengeResponse: 'j***@a***', + }); + + expect(setupEmailConfirmSignInResult.nextStep.signInStep).toBe( + 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE', + ); + + jest + .spyOn(signInHelpers, 'handleChallengeName') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeCommandOutput, + ); + + const confirmSignInResult = await confirmSignIn({ + challengeResponse: '123456', + }); + + expect(confirmSignInResult.isSignedIn).toBe(true); + expect(confirmSignInResult.nextStep.signInStep).toBe('DONE'); + }); + + test('confirmSignIn with single MFA_SETUP option using SOFTWARE_TOKEN_MFA', async () => { + Amplify.configure({ + Auth: authConfig, + }); + jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeTotpMfaSetupOutput, + ); + + jest.spyOn(clients, 'associateSoftwareToken').mockResolvedValueOnce({ + SecretCode: 'secret-code', + Session: '12341234', + $metadata: {}, + }); + + const result = await signIn({ username, password }); + + expect(result.isSignedIn).toBe(false); + expect(result.nextStep.signInStep).toBe('CONTINUE_SIGN_IN_WITH_TOTP_SETUP'); + + const verifySoftwareTokenSpy = jest + .spyOn(clients, 'verifySoftwareToken') + .mockResolvedValueOnce({ + Session: '12341234', + Status: 'SUCCESS', + $metadata: {}, + }); + + jest + .spyOn(clients, 'respondToAuthChallenge') + .mockImplementationOnce( + async (): Promise => + authAPITestParams.RespondToAuthChallengeCommandOutput, + ); + + const totpCode = '123456'; + const confirmSignInResult = await confirmSignIn({ + challengeResponse: totpCode, + }); + + expect(verifySoftwareTokenSpy).toHaveBeenCalledWith( + expect.objectContaining({ + region: 'us-west-2', + }), + expect.objectContaining({ + UserCode: totpCode, + Session: '12341234', + }), + ); + expect(confirmSignInResult.isSignedIn).toBe(true); + expect(confirmSignInResult.nextStep.signInStep).toBe('DONE'); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts b/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts index b4e8453b17d..fee490b8bfa 100644 --- a/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts @@ -9,6 +9,8 @@ import { getCurrentUser, signIn } from '../../../src/providers/cognito'; import { initiateAuth } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; import { InitiateAuthException } from '../../../src/providers/cognito/types/errors'; import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../src/errors/constants'; +import { AuthErrorCodes } from '../../../src/common/AuthErrorStrings'; +import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError } from './testUtils/data'; @@ -97,4 +99,28 @@ describe('signIn API error path cases:', () => { expect(error.name).toBe(InitiateAuthException.InvalidParameterException); } }); + it('should throw an error when sign in step is MFA_SETUP and there are no valid setup options', async () => { + expect.assertions(3); + + jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce(async () => ({ + ChallengeName: 'MFA_SETUP', + ChallengeParameters: { + MFAS_CAN_SETUP: '["SMS_MFA"]', + }, + $metadata: {}, + })); + + try { + await signIn({ + username: authAPITestParams.user1.username, + password: authAPITestParams.user1.password, + }); + } catch (error: any) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe(AuthErrorCodes.SignInException); + expect(error.message).toContain('SMS'); + } + }); }); diff --git a/packages/auth/__tests__/providers/cognito/testUtils/authApiTestParams.ts b/packages/auth/__tests__/providers/cognito/testUtils/authApiTestParams.ts index 9d5cde07f27..1719bb8d9a4 100644 --- a/packages/auth/__tests__/providers/cognito/testUtils/authApiTestParams.ts +++ b/packages/auth/__tests__/providers/cognito/testUtils/authApiTestParams.ts @@ -112,6 +112,30 @@ export const authAPITestParams = { Session: 'aaabbbcccddd', $metadata: {}, }, + RespondToAuthChallengeMultipleMfaSetupOutput: { + ChallengeName: 'MFA_SETUP', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + MFAS_CAN_SETUP: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]', + }, + }, + RespondToAuthChallengeEmailMfaSetupOutput: { + ChallengeName: 'MFA_SETUP', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + MFAS_CAN_SETUP: '["SMS_MFA", "EMAIL_OTP"]', + }, + }, + RespondToAuthChallengeTotpMfaSetupOutput: { + ChallengeName: 'MFA_SETUP', + Session: '1234234232', + $metadata: {}, + ChallengeParameters: { + MFAS_CAN_SETUP: '["SMS_MFA", "SOFTWARE_TOKEN_MFA"]', + }, + }, CustomChallengeResponse: { ChallengeName: 'CUSTOM_CHALLENGE', AuthenticationResult: undefined, @@ -199,7 +223,6 @@ export const authAPITestParams = { }, GuestIdentityId: { id: 'guest-identity-id', type: 'guest' }, PrimaryIdentityId: { id: 'primary-identity-id', type: 'primary' }, - signInResultWithCustomAuth: () => { return { isSignedIn: false, diff --git a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts index 6aad224af30..badbdf7850e 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts @@ -71,8 +71,8 @@ export async function confirmSignIn( throw new AuthError({ name: AuthErrorCodes.SignInException, message: ` - An error occurred during the sign in process. - + An error occurred during the sign in process. + This most likely occurred due to: 1. signIn was not called before confirmSignIn. 2. signIn threw an exception. diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 99a615644b5..5b6ab56b210 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -148,36 +148,86 @@ export async function handleMFASetupChallenge({ config, }: HandleAuthChallengeRequest): Promise { const { userPoolId, userPoolClientId } = config; - const challengeResponses = { - USERNAME: username, - }; - const { Session } = await verifySoftwareToken( - { - region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), - }, - { - UserCode: challengeResponse, + if (challengeResponse === 'EMAIL') { + return { + ChallengeName: 'MFA_SETUP', Session: session, - FriendlyDeviceName: deviceName, - }, - ); + ChallengeParameters: { + MFAS_CAN_SETUP: '["EMAIL_OTP"]', + }, + $metadata: {}, + }; + } - signInStore.dispatch({ - type: 'SET_SIGN_IN_SESSION', - value: Session, - }); + if (challengeResponse === 'TOTP') { + return { + ChallengeName: 'MFA_SETUP', + Session: session, + ChallengeParameters: { + MFAS_CAN_SETUP: '["SOFTWARE_TOKEN_MFA"]', + }, + $metadata: {}, + }; + } - const jsonReq: RespondToAuthChallengeCommandInput = { - ChallengeName: 'MFA_SETUP', - ChallengeResponses: challengeResponses, - Session, - ClientMetadata: clientMetadata, - ClientId: userPoolClientId, + const challengeResponses: Record = { + USERNAME: username, }; - return respondToAuthChallenge({ region: getRegion(userPoolId) }, jsonReq); + const isTOTPCode = /^\d+$/.test(challengeResponse.trim()); + + if (isTOTPCode) { + const { Session } = await verifySoftwareToken( + { + region: getRegion(userPoolId), + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, + { + UserCode: challengeResponse, + Session: session, + FriendlyDeviceName: deviceName, + }, + ); + + signInStore.dispatch({ + type: 'SET_SIGN_IN_SESSION', + value: Session, + }); + + const jsonReq: RespondToAuthChallengeCommandInput = { + ChallengeName: 'MFA_SETUP', + ChallengeResponses: challengeResponses, + Session, + ClientMetadata: clientMetadata, + ClientId: userPoolClientId, + }; + + return respondToAuthChallenge({ region: getRegion(userPoolId) }, jsonReq); + } + + const isEmail = /^\S+@\S+\.\S+$/.test(challengeResponse.trim()); + + if (isEmail) { + challengeResponses.EMAIL = challengeResponse; + + const jsonReq: RespondToAuthChallengeCommandInput = { + ChallengeName: 'MFA_SETUP', + ChallengeResponses: challengeResponses, + Session: session, + ClientMetadata: clientMetadata, + ClientId: userPoolClientId, + }; + + return respondToAuthChallenge({ region: getRegion(userPoolId) }, jsonReq); + } + + throw new AuthError({ + name: AuthErrorCodes.SignInException, + message: `Cannot proceed with MFA setup using challengeResponse: ${challengeResponse}`, + recoverySuggestion: + 'Try passing "EMAIL", "TOTP", a valid email, or OTP code as the challengeResponse.', + }); } export async function handleSelectMFATypeChallenge({ @@ -691,31 +741,60 @@ export async function getSignInResult(params: { case 'MFA_SETUP': { const { signInSession, username } = signInStore.getState(); - if (!isMFATypeEnabled(challengeParameters, 'TOTP')) - throw new AuthError({ - name: AuthErrorCodes.SignInException, - message: `Cannot initiate MFA setup from available types: ${getMFATypes( - parseMFATypes(challengeParameters.MFAS_CAN_SETUP), - )}`, + const mfaSetupTypes = + getMFATypes(parseMFATypes(challengeParameters.MFAS_CAN_SETUP)) || []; + + const allowedMfaSetupTypes = getAllowedMfaSetupTypes(mfaSetupTypes); + + const isTotpMfaSetupAvailable = allowedMfaSetupTypes.includes('TOTP'); + const isEmailMfaSetupAvailable = allowedMfaSetupTypes.includes('EMAIL'); + + if (isTotpMfaSetupAvailable && isEmailMfaSetupAvailable) { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION', + allowedMFATypes: allowedMfaSetupTypes, + }, + }; + } + + if (isEmailMfaSetupAvailable) { + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP', + }, + }; + } + + if (isTotpMfaSetupAvailable) { + const { Session, SecretCode: secretCode } = + await associateSoftwareToken( + { region: getRegion(authConfig.userPoolId) }, + { + Session: signInSession, + }, + ); + + signInStore.dispatch({ + type: 'SET_SIGN_IN_SESSION', + value: Session, }); - const { Session, SecretCode: secretCode } = await associateSoftwareToken( - { region: getRegion(authConfig.userPoolId) }, - { - Session: signInSession, - }, - ); - signInStore.dispatch({ - type: 'SET_SIGN_IN_SESSION', - value: Session, - }); - return { - isSignedIn: false, - nextStep: { - signInStep: 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP', - totpSetupDetails: getTOTPSetupDetails(secretCode!, username), - }, - }; + return { + isSignedIn: false, + nextStep: { + signInStep: 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP', + totpSetupDetails: getTOTPSetupDetails(secretCode!, username), + }, + }; + } + + throw new AuthError({ + name: AuthErrorCodes.SignInException, + message: `Cannot initiate MFA setup from available types: ${mfaSetupTypes}`, + }); } case 'NEW_PASSWORD_REQUIRED': return { @@ -912,7 +991,7 @@ export async function handleChallengeName( // TODO: remove this error message for production apps throw new AuthError({ name: AuthErrorCodes.SignInException, - message: `An error occurred during the sign in process. + message: `An error occurred during the sign in process. ${challengeName} challengeName returned by the underlying service was not addressed.`, }); } @@ -943,15 +1022,10 @@ export function parseMFATypes(mfa?: string): CognitoMFAType[] { return JSON.parse(mfa) as CognitoMFAType[]; } -export function isMFATypeEnabled( - challengeParams: ChallengeParameters, - mfaType: AuthMFAType, -): boolean { - const { MFAS_CAN_SETUP } = challengeParams; - const mfaTypes = getMFATypes(parseMFATypes(MFAS_CAN_SETUP)); - if (!mfaTypes) return false; - - return mfaTypes.includes(mfaType); +export function getAllowedMfaSetupTypes(availableMfaSetupTypes: AuthMFAType[]) { + return availableMfaSetupTypes.filter( + authMfaType => authMfaType === 'EMAIL' || authMfaType === 'TOTP', + ); } export async function assertUserNotAuthenticated() { diff --git a/packages/auth/src/types/models.ts b/packages/auth/src/types/models.ts index 0e671266524..e08b7bce5f9 100644 --- a/packages/auth/src/types/models.ts +++ b/packages/auth/src/types/models.ts @@ -63,6 +63,20 @@ export interface ContinueSignInWithTOTPSetup { signInStep: 'CONTINUE_SIGN_IN_WITH_TOTP_SETUP'; totpSetupDetails: AuthTOTPSetupDetails; } +export interface ContinueSignInWithEmailSetup { + /** + * Auth step requires user to set up EMAIL as multifactor authentication by associating an email address + * and entering the OTP. + * + * @example + * ```typescript + * // Code retrieved from email + * const emailAddress = 'example@example.com'; + * await confirmSignIn({challengeResponse: emailAddress }); + * ``` + */ + signInStep: 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP'; +} export interface ConfirmSignInWithTOTPCode { /** * Auth step requires user to use TOTP as multifactor authentication by retriving an OTP code from authenticator app. @@ -92,6 +106,21 @@ export interface ContinueSignInWithMFASelection { allowedMFATypes?: AuthAllowedMFATypes; } +export interface ContinueSignInWithMFASetupSelection { + /** + * Auth step requires user to select an mfa option (SMS | TOTP) to setup before continuing the sign-in flow. + * + * @example + * ```typescript + * await confirmSignIn({challengeResponse:'TOTP'}); + * // OR + * await confirmSignIn({challengeResponse:'EMAIL'}); + * ``` + */ + signInStep: 'CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION'; + allowedMFATypes?: AuthAllowedMFATypes; +} + export interface ConfirmSignInWithCustomChallenge { /** * Auth step requires user to respond to a custom challenge. @@ -198,6 +227,8 @@ export type AuthNextSignInStep< | ConfirmSignInWithTOTPCode | ConfirmSignInWithEmailCode | ContinueSignInWithTOTPSetup + | ContinueSignInWithEmailSetup + | ContinueSignInWithMFASetupSelection | ConfirmSignUpStep | ResetPasswordStep | DoneSignInStep; From 6da06521e0426111051ea09af0689d37b89bbdad Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 23 Sep 2024 14:38:16 -0400 Subject: [PATCH 04/11] feat(auth): [EMAIL MFA] enable integ tests with backend configuration swapping (#13794) * chore: enable mfa integ tests * chore: add mfa-setup test def --- .github/integ-config/integ-all.yml | 45 ++++++++++++++++++++++++ .github/workflows/callable-e2e-test.yml | 11 ++++-- .github/workflows/callable-e2e-tests.yml | 1 + .github/workflows/push-integ-test.yml | 2 +- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index e176add9ac5..d2cfff3fdc2 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -877,3 +877,48 @@ tests: sample_name: auth-gql-storage yarn_script: ci:node-env-test node_versions: ['18.x', '20.x', '22.x'] + - test_name: integ_next_mfa_req_email + desc: 'mfa required with email sign in attribute' + framework: next + category: auth + sample_name: [mfa] + spec: mfa-req-email + browser: [chrome] + env: + NEXT_PUBLIC_BACKEND_CONFIG: mfa-req-email + - test_name: integ_next_mfa_req_phone + desc: 'mfa required with phone sign in attribute' + framework: next + category: auth + sample_name: [mfa] + spec: mfa-req-phone + browser: [chrome] + env: + NEXT_PUBLIC_BACKEND_CONFIG: mfa-req-phone + - test_name: integ_next_mfa_opt_email + desc: 'mfa optional with email sign in attribute' + framework: next + category: auth + sample_name: [mfa] + spec: mfa-opt-email + browser: [chrome] + env: + NEXT_PUBLIC_BACKEND_CONFIG: mfa-opt-email + - test_name: integ_next_mfa_opt_phone + desc: 'mfa optional with phone sign in attribute' + framework: next + category: auth + sample_name: [mfa] + spec: mfa-opt-phone + browser: [chrome] + env: + NEXT_PUBLIC_BACKEND_CONFIG: mfa-opt-phone + - test_name: integ_next_mfa_setup + desc: 'mfa setup sign in flow' + framework: next + category: auth + sample_name: [mfa] + spec: mfa-setup + browser: [chrome] + env: + NEXT_PUBLIC_BACKEND_CONFIG: mfa-setup diff --git a/.github/workflows/callable-e2e-test.yml b/.github/workflows/callable-e2e-test.yml index fd9f9cb697a..403e3c8c91d 100644 --- a/.github/workflows/callable-e2e-test.yml +++ b/.github/workflows/callable-e2e-test.yml @@ -40,6 +40,9 @@ on: node_versions: required: false type: string + env: + required: false + type: string env: AMPLIFY_DIR: /home/runner/work/amplify-js/amplify-js/amplify-js @@ -91,6 +94,7 @@ jobs: E2E_RETRY_COUNT: ${{ inputs.retry_count }} E2E_TEST_NAME: ${{ inputs.test_name }} E2E_YARN_SCRIPT: ${{ inputs.yarn_script }} + E2E_ENV: ${{ inputs.env }} run: | if [ -z "$E2E_YARN_SCRIPT" ]; then ../amplify-js/scripts/retry-yarn-script.sh -s \ @@ -102,7 +106,8 @@ jobs: $E2E_BROWSER \ dev \ $E2E_BACKEND \ - $E2E_AMPLIFY_JS_DIR" \ + $E2E_AMPLIFY_JS_DIR \ + --env $(echo $E2E_ENV | jq -r 'tostring')" \ $E2E_YARN_SCRIPT \ -n $E2E_RETRY_COUNT else @@ -122,6 +127,7 @@ jobs: E2E_RETRY_COUNT: ${{ inputs.retry_count }} E2E_TEST_NAME: ${{ inputs.test_name }} E2E_YARN_SCRIPT: ${{ inputs.yarn_script }} + E2E_ENV: ${{ inputs.env }} run: | if [ -z "$E2E_YARN_SCRIPT" ]; then ../amplify-js/scripts/retry-yarn-script.sh -s \ @@ -133,7 +139,8 @@ jobs: $E2E_BROWSER \ prod \ $E2E_BACKEND \ - $E2E_AMPLIFY_JS_DIR" \ + $E2E_AMPLIFY_JS_DIR \ + --env $(echo $E2E_ENV | jq -r 'tostring')" \ $E2E_YARN_SCRIPT \ -n $E2E_RETRY_COUNT else diff --git a/.github/workflows/callable-e2e-tests.yml b/.github/workflows/callable-e2e-tests.yml index 477ca28b3db..91a37bf0e3c 100644 --- a/.github/workflows/callable-e2e-tests.yml +++ b/.github/workflows/callable-e2e-tests.yml @@ -45,6 +45,7 @@ jobs: retry_count: ${{ matrix.integ-config.retry_count || 3 }} yarn_script: ${{ matrix.integ-config.yarn_script || '' }} node_versions: ${{ toJSON(matrix.integ-config.node_versions) || '[""]' }} + env: ${{ matrix.integ-config.env && toJSON(matrix.integ-config.env) || '{}' }} # e2e-test-runner-headless: # name: E2E test runnner_headless diff --git a/.github/workflows/push-integ-test.yml b/.github/workflows/push-integ-test.yml index 03e43dd2865..a88068c2c89 100644 --- a/.github/workflows/push-integ-test.yml +++ b/.github/workflows/push-integ-test.yml @@ -8,7 +8,7 @@ concurrency: on: push: branches: - - replace-with-your-branch + - test/email-mfa-integ jobs: e2e: From 93bea39ccb005aaf84f20cfe725f0620412a74c4 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Mon, 21 Oct 2024 16:44:31 -0700 Subject: [PATCH 05/11] chore: temporarily enable push integ tests --- .github/workflows/push-integ-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push-integ-test.yml b/.github/workflows/push-integ-test.yml index a88068c2c89..061f52811e0 100644 --- a/.github/workflows/push-integ-test.yml +++ b/.github/workflows/push-integ-test.yml @@ -8,7 +8,7 @@ concurrency: on: push: branches: - - test/email-mfa-integ + - feat/email-mfa jobs: e2e: From 2fa41ae847bf8e935db2e13c6046f16b6211a25f Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Tue, 22 Oct 2024 09:09:21 -0700 Subject: [PATCH 06/11] chore: disable push integ tests --- .github/workflows/push-integ-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push-integ-test.yml b/.github/workflows/push-integ-test.yml index 061f52811e0..03e43dd2865 100644 --- a/.github/workflows/push-integ-test.yml +++ b/.github/workflows/push-integ-test.yml @@ -8,7 +8,7 @@ concurrency: on: push: branches: - - feat/email-mfa + - replace-with-your-branch jobs: e2e: From 34933365510e1b3e54c1284c2d214e08897b6c12 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Thu, 24 Oct 2024 13:19:40 -0700 Subject: [PATCH 07/11] chore: address test strategy feedback --- .../cognito/signInErrorCases.test.ts | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts b/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts index 6197716502b..4ebdc2f0a8f 100644 --- a/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts @@ -8,9 +8,8 @@ import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { getCurrentUser, signIn } from '../../../src/providers/cognito'; import { InitiateAuthException } from '../../../src/providers/cognito/types/errors'; import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../src/errors/constants'; -import { AuthErrorCodes } from '../../../src/common/AuthErrorStrings'; -import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers'; import { createInitiateAuthClient } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider'; +import { AuthErrorCodes } from '../../../src/common/AuthErrorStrings'; import { authAPITestParams } from './testUtils/authApiTestParams'; import { getMockError } from './testUtils/data'; @@ -28,11 +27,13 @@ jest.mock('../../../src/providers/cognito/apis/getCurrentUser'); jest.mock( '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider', ); +jest.mock('../../../src/providers/cognito/tokenProvider'); describe('signIn API error path cases:', () => { // assert mocks const mockCreateInitiateAuthClient = jest.mocked(createInitiateAuthClient); const mockInitiateAuth = jest.fn(); + const mockedGetCurrentUser = getCurrentUser as jest.Mock; beforeAll(() => { @@ -45,7 +46,7 @@ describe('signIn API error path cases:', () => { afterEach(() => { mockedGetCurrentUser.mockReset(); - mockInitiateAuth.mockClear(); + mockInitiateAuth.mockReset(); }); it('should throw an error when a user is already signed-in', async () => { @@ -90,42 +91,44 @@ describe('signIn API error path cases:', () => { }); it('should throw an error when service returns an error response', async () => { - expect.assertions(2); mockInitiateAuth.mockImplementation(() => { throw getMockError(InitiateAuthException.InvalidParameterException); }); - try { - await signIn({ - username: authAPITestParams.user1.username, - password: authAPITestParams.user1.password, - }); - } catch (error: any) { - expect(error).toBeInstanceOf(AuthError); - expect(error.name).toBe(InitiateAuthException.InvalidParameterException); - } + + const p = signIn({ + username: authAPITestParams.user1.username, + password: authAPITestParams.user1.password, + }); + + expect(p).rejects.toThrow( + new AuthError({ + name: InitiateAuthException.InvalidParameterException, + message: 'Error message', + }), + ); }); it('should throw an error when sign in step is MFA_SETUP and there are no valid setup options', async () => { - expect.assertions(3); - - jest - .spyOn(signInHelpers, 'handleUserSRPAuthFlow') - .mockImplementationOnce(async () => ({ - ChallengeName: 'MFA_SETUP', - ChallengeParameters: { - MFAS_CAN_SETUP: '["SMS_MFA"]', - }, - $metadata: {}, - })); + mockInitiateAuth.mockImplementation(() => ({ + ChallengeName: 'MFA_SETUP', + ChallengeParameters: { + MFAS_CAN_SETUP: '["SMS_MFA"]', + }, + $metadata: {}, + })); - try { - await signIn({ - username: authAPITestParams.user1.username, - password: authAPITestParams.user1.password, - }); - } catch (error: any) { - expect(error).toBeInstanceOf(AuthError); - expect(error.name).toBe(AuthErrorCodes.SignInException); - expect(error.message).toContain('SMS'); - } + const p = signIn({ + username: authAPITestParams.user1.username, + password: authAPITestParams.user1.password, + options: { + authFlowType: 'USER_PASSWORD_AUTH', + }, + }); + + expect(p).rejects.toThrow( + new AuthError({ + name: AuthErrorCodes.SignInException, + message: 'Cannot initiate MFA setup from available types: SMS', + }), + ); }); }); From 3258ee91bb65eb2cb862fecef9eda3e254100a74 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Thu, 24 Oct 2024 14:28:33 -0700 Subject: [PATCH 08/11] chore: use trimmed challenge response --- .../providers/cognito/utils/signInHelpers.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 1b7ed9e9051..e2646babf30 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -155,7 +155,9 @@ export async function handleMFASetupChallenge({ }: HandleAuthChallengeRequest): Promise { const { userPoolId, userPoolClientId, userPoolEndpoint } = config; - if (challengeResponse === 'EMAIL') { + const trimmedChallengeResponse = challengeResponse.trim(); + + if (trimmedChallengeResponse === 'EMAIL') { return { ChallengeName: 'MFA_SETUP', Session: session, @@ -166,7 +168,7 @@ export async function handleMFASetupChallenge({ }; } - if (challengeResponse === 'TOTP') { + if (trimmedChallengeResponse === 'TOTP') { return { ChallengeName: 'MFA_SETUP', Session: session, @@ -181,7 +183,7 @@ export async function handleMFASetupChallenge({ USERNAME: username, }; - const isTOTPCode = /^\d+$/.test(challengeResponse.trim()); + const isTOTPCode = /^\d+$/.test(trimmedChallengeResponse); if (isTOTPCode) { const verifySoftwareToken = createVerifySoftwareTokenClient({ @@ -196,7 +198,7 @@ export async function handleMFASetupChallenge({ userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), }, { - UserCode: challengeResponse, + UserCode: trimmedChallengeResponse, Session: session, FriendlyDeviceName: deviceName, }, @@ -222,15 +224,18 @@ export async function handleMFASetupChallenge({ }); return respondToAuthChallenge( - { region: getRegionFromUserPoolId(userPoolId) }, + { + region: getRegionFromUserPoolId(userPoolId), + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, jsonReq, ); } - const isEmail = /^\S+@\S+\.\S+$/.test(challengeResponse.trim()); + const isEmail = trimmedChallengeResponse.includes('@'); if (isEmail) { - challengeResponses.EMAIL = challengeResponse; + challengeResponses.EMAIL = trimmedChallengeResponse; const jsonReq: RespondToAuthChallengeCommandInput = { ChallengeName: 'MFA_SETUP', @@ -247,7 +252,10 @@ export async function handleMFASetupChallenge({ }); return respondToAuthChallenge( - { region: getRegionFromUserPoolId(userPoolId) }, + { + region: getRegionFromUserPoolId(userPoolId), + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, jsonReq, ); } From 026d8de2184def52ceb4b4246b79e73bb5193da4 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Thu, 24 Oct 2024 15:09:47 -0700 Subject: [PATCH 09/11] chore: improved naming --- .../__tests__/providers/cognito/signInErrorCases.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts b/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts index 4ebdc2f0a8f..94b4029418b 100644 --- a/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts @@ -95,12 +95,12 @@ describe('signIn API error path cases:', () => { throw getMockError(InitiateAuthException.InvalidParameterException); }); - const p = signIn({ + const signInResultPromise = signIn({ username: authAPITestParams.user1.username, password: authAPITestParams.user1.password, }); - expect(p).rejects.toThrow( + expect(signInResultPromise).rejects.toThrow( new AuthError({ name: InitiateAuthException.InvalidParameterException, message: 'Error message', @@ -116,7 +116,7 @@ describe('signIn API error path cases:', () => { $metadata: {}, })); - const p = signIn({ + const signInResultPromise = signIn({ username: authAPITestParams.user1.username, password: authAPITestParams.user1.password, options: { @@ -124,7 +124,7 @@ describe('signIn API error path cases:', () => { }, }); - expect(p).rejects.toThrow( + expect(signInResultPromise).rejects.toThrow( new AuthError({ name: AuthErrorCodes.SignInException, message: 'Cannot initiate MFA setup from available types: SMS', From d2d44706e565da7619fab97ff80832a3393c05fa Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Wed, 30 Oct 2024 18:07:17 -0700 Subject: [PATCH 10/11] chore: update bundle size tests --- packages/aws-amplify/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index d39dd6e0fad..cc14fb0b487 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -389,7 +389,7 @@ "name": "[Auth] updateMFAPreference (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ updateMFAPreference }", - "limit": "11.98 kB" + "limit": "12.00 kB" }, { "name": "[Auth] fetchMFAPreference (Cognito)", From 8f46f05e4fe9737f7ae8ff46b30b10aa8eb25dc1 Mon Sep 17 00:00:00 2001 From: James Jarvis Date: Thu, 31 Oct 2024 10:34:31 -0700 Subject: [PATCH 11/11] chore: remove trimmed challenge response --- .../src/providers/cognito/utils/signInHelpers.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index e2646babf30..d3bce2aa6f2 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -155,9 +155,7 @@ export async function handleMFASetupChallenge({ }: HandleAuthChallengeRequest): Promise { const { userPoolId, userPoolClientId, userPoolEndpoint } = config; - const trimmedChallengeResponse = challengeResponse.trim(); - - if (trimmedChallengeResponse === 'EMAIL') { + if (challengeResponse === 'EMAIL') { return { ChallengeName: 'MFA_SETUP', Session: session, @@ -168,7 +166,7 @@ export async function handleMFASetupChallenge({ }; } - if (trimmedChallengeResponse === 'TOTP') { + if (challengeResponse === 'TOTP') { return { ChallengeName: 'MFA_SETUP', Session: session, @@ -183,7 +181,7 @@ export async function handleMFASetupChallenge({ USERNAME: username, }; - const isTOTPCode = /^\d+$/.test(trimmedChallengeResponse); + const isTOTPCode = /^\d+$/.test(challengeResponse); if (isTOTPCode) { const verifySoftwareToken = createVerifySoftwareTokenClient({ @@ -198,7 +196,7 @@ export async function handleMFASetupChallenge({ userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), }, { - UserCode: trimmedChallengeResponse, + UserCode: challengeResponse, Session: session, FriendlyDeviceName: deviceName, }, @@ -232,10 +230,10 @@ export async function handleMFASetupChallenge({ ); } - const isEmail = trimmedChallengeResponse.includes('@'); + const isEmail = challengeResponse.includes('@'); if (isEmail) { - challengeResponses.EMAIL = trimmedChallengeResponse; + challengeResponses.EMAIL = challengeResponse; const jsonReq: RespondToAuthChallengeCommandInput = { ChallengeName: 'MFA_SETUP',