diff --git a/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts b/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts index 622f6e12b8c..d281c3a6d3f 100644 --- a/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts @@ -3,20 +3,11 @@ import { Amplify } from '@aws-amplify/core'; -import { - confirmSignIn, - getCurrentUser, - signIn, -} from '../../../src/providers/cognito'; +import { getCurrentUser, signIn } from '../../../src/providers/cognito'; import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers'; -import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; import { signInStore } from '../../../src/providers/cognito/utils/signInStore'; -import { - AssociateSoftwareTokenCommandOutput, - RespondToAuthChallengeCommandOutput, -} from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/types'; +import { RespondToAuthChallengeCommandOutput } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/types'; import { cognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider'; -import { mfaSetupStore } from '../../../src/providers/cognito/utils/mfaSetupStore'; import { authAPITestParams } from './testUtils/authApiTestParams'; @@ -120,190 +111,3 @@ describe('local sign-in state management tests', () => { mockedGetCurrentUser.mockClear(); }); }); - -describe('mfa setup state management tests', () => { - const handleChallengeNameSpy = jest.spyOn( - signInHelpers, - 'handleChallengeName', - ); - const handleUserSRPAuthFlowSpy = jest.spyOn( - signInHelpers, - 'handleUserSRPAuthFlow', - ); - - beforeAll(() => { - Amplify.configure({ Auth: authConfig }); - }); - - test('mfa setup state should initialize as undefined', () => { - const mfaSetupState = mfaSetupStore.getState(); - - expect(mfaSetupState).toStrictEqual(undefined); - }); - - test('mfa setup state should be updated with options during first MFA_SETUP challenge', async () => { - jest.spyOn(signInHelpers, 'handleUserSRPAuthFlow').mockImplementationOnce( - async (): Promise => ({ - ChallengeName: 'MFA_SETUP', - Session: '1234234232', - $metadata: {}, - ChallengeParameters: { - MFAS_CAN_SETUP: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]', - }, - }), - ); - - await signIn({ username, password }); - - const mfaSetupState = mfaSetupStore.getState(); - - expect(mfaSetupState).toStrictEqual({ - status: 'IN_PROGRESS', - options: ['TOTP', 'EMAIL'], - }); - }); - - test('mfa setup state should be updated with selected value during second MFA_SETUP challenge', async () => { - jest.spyOn(signInHelpers, 'handleUserSRPAuthFlow').mockImplementationOnce( - async (): Promise => ({ - ChallengeName: 'MFA_SETUP', - Session: '1234234232', - $metadata: {}, - ChallengeParameters: { - MFAS_CAN_SETUP: '["SMS_MFA","SOFTWARE_TOKEN_MFA", "EMAIL_OTP"]', - }, - }), - ); - - await signIn({ username, password }); - - await confirmSignIn({ - challengeResponse: 'EMAIL', - }); - - expect(mfaSetupStore.getState()).toStrictEqual({ - status: 'COMPLETE', - options: ['TOTP', 'EMAIL'], - value: 'EMAIL', - }); - }); - - test('mfa setup state should be cleared with successful sign in', async () => { - handleUserSRPAuthFlowSpy.mockImplementationOnce( - async (): Promise => - authAPITestParams.RespondToAuthChallengeMultipleMfaSetupOutput, - ); - - await signIn({ username, password }); - - await confirmSignIn({ - challengeResponse: 'EMAIL', - }); - - handleChallengeNameSpy.mockImplementationOnce( - async (): Promise => ({ - ChallengeName: 'EMAIL_OTP', - Session: '1234234232', - $metadata: {}, - ChallengeParameters: { - CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL', - CODE_DELIVERY_DESTINATION: 'j***@a***', - }, - }), - ); - - await confirmSignIn({ - challengeResponse: 'j***@a***', - }); - - handleChallengeNameSpy.mockImplementationOnce( - async (): Promise => - authAPITestParams.RespondToAuthChallengeCommandOutput, - ); - - await confirmSignIn({ - challengeResponse: '123456', - }); - - const mfaSetupState = mfaSetupStore.getState(); - - expect(mfaSetupState).toStrictEqual(undefined); - }); - - test('mfa setup state should be reset with each sign in attempt', async () => { - handleUserSRPAuthFlowSpy.mockImplementationOnce( - async (): Promise => - authAPITestParams.RespondToAuthChallengeMultipleMfaSetupOutput, - ); - - await signIn({ username, password }); - - await confirmSignIn({ - challengeResponse: 'EMAIL', - }); - - expect(mfaSetupStore.getState()).toStrictEqual({ - status: 'COMPLETE', - options: ['TOTP', 'EMAIL'], - value: 'EMAIL', - }); - - handleUserSRPAuthFlowSpy.mockImplementationOnce( - async (): Promise => - authAPITestParams.RespondToAuthChallengeCommandOutput, - ); - - await signIn({ username, password }); - - expect(mfaSetupStore.getState()).toStrictEqual(undefined); - }); - - test('mfa setup state should autocomplete when only one allowed MFA setup option is available (EMAIL_OTP)', async () => { - handleUserSRPAuthFlowSpy.mockImplementationOnce( - async (): Promise => ({ - ChallengeName: 'MFA_SETUP', - Session: '1234234232', - $metadata: {}, - ChallengeParameters: { - MFAS_CAN_SETUP: '["SMS_MFA", "EMAIL_OTP"]', - }, - }), - ); - - await signIn({ username, password }); - - expect(mfaSetupStore.getState()).toStrictEqual({ - status: 'COMPLETE', - options: ['EMAIL'], - value: 'EMAIL', - }); - }); - test('mfa setup state should autocomplete when only one allowed MFA setup option is available (SOFTWARE_TOKEN_MFA)', async () => { - handleUserSRPAuthFlowSpy.mockImplementationOnce( - async (): Promise => ({ - ChallengeName: 'MFA_SETUP', - Session: '1234234232', - $metadata: {}, - ChallengeParameters: { - MFAS_CAN_SETUP: '["SMS_MFA", "SOFTWARE_TOKEN_MFA"]', - }, - }), - ); - - jest.spyOn(clients, 'associateSoftwareToken').mockImplementationOnce( - async (): Promise => ({ - SecretCode: 'secret-code', - Session: '12341234', - $metadata: {}, - }), - ); - - await signIn({ username, password }); - - expect(mfaSetupStore.getState()).toStrictEqual({ - status: 'COMPLETE', - options: ['TOTP'], - value: 'TOTP', - }); - }); -}); diff --git a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts index 68037e56bcf..badbdf7850e 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts @@ -33,7 +33,6 @@ import { } from '../utils/clients/CognitoIdentityProvider/types'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; -import { resetMfaSetupState } from '../utils/mfaSetupStore'; /** * Continues or completes the sign in process when required by the initial call to `signIn`. @@ -111,7 +110,6 @@ export async function confirmSignIn( if (AuthenticationResult) { cleanActiveSignInState(); - resetMfaSetupState(); await cacheCognitoTokens({ username, ...AuthenticationResult, diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts index 56c12c3c0d7..e55e3f0a50d 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts @@ -32,7 +32,6 @@ import { } from '../utils/clients/CognitoIdentityProvider/types'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; -import { resetMfaSetupState } from '../utils/mfaSetupStore'; /** * Signs a user in using a custom authentication flow without password @@ -65,7 +64,6 @@ export async function signInWithCustomAuth( ); try { - resetMfaSetupState(); const { ChallengeName: retriedChallengeName, ChallengeParameters: retiredChallengeParameters, diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts index 3a09dc451d0..a67fa9f861c 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts @@ -34,7 +34,6 @@ import { } from '../utils/clients/CognitoIdentityProvider/types'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; -import { resetMfaSetupState } from '../utils/mfaSetupStore'; /** * Signs a user in using a custom authentication flow with SRP @@ -68,7 +67,6 @@ export async function signInWithCustomSRPAuth( ); try { - resetMfaSetupState(); const { ChallengeName: handledChallengeName, ChallengeParameters: handledChallengeParameters, diff --git a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts index 55803dc93f6..32f0ca11b99 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts @@ -34,7 +34,6 @@ import { import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; -import { resetMfaSetupState } from '../utils/mfaSetupStore'; /** * Signs a user in @@ -68,7 +67,6 @@ export async function signInWithSRP( ); try { - resetMfaSetupState(); const { ChallengeName: handledChallengeName, ChallengeParameters: handledChallengeParameters, diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts index 4d6fcde107b..e1de730cb1c 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts @@ -32,7 +32,6 @@ import { import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; -import { resetMfaSetupState } from '../utils/mfaSetupStore'; /** * Signs a user in using USER_PASSWORD_AUTH AuthFlowType @@ -65,7 +64,6 @@ export async function signInWithUserPassword( ); try { - resetMfaSetupState(); const { ChallengeName: retiredChallengeName, ChallengeParameters: retriedChallengeParameters, diff --git a/packages/auth/src/providers/cognito/utils/mfaSetupStore.ts b/packages/auth/src/providers/cognito/utils/mfaSetupStore.ts deleted file mode 100644 index fdb564f0d13..00000000000 --- a/packages/auth/src/providers/cognito/utils/mfaSetupStore.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AuthMFAType } from '../../../types'; - -import { Reducer, Store } from './types'; - -type MfaSetupInitial = undefined; - -interface MfaSetupSelectionRequired { - status: 'IN_PROGRESS'; - options: AuthMFAType[]; -} -interface MfaSetupSelectionComplete { - status: 'COMPLETE'; - value: AuthMFAType; - options: AuthMFAType[]; -} - -type MfaSetupState = - | MfaSetupInitial - | MfaSetupSelectionRequired - | MfaSetupSelectionComplete; - -type MfaSetupAction = - | { type: 'RESET' } - | { type: 'IN_PROGRESS'; value: AuthMFAType[] } - | { type: 'COMPLETE'; value: AuthMFAType } - | { type: 'AUTO'; value: Omit }; - -const mfaSetupReducer: Reducer = ( - state, - action, -) => { - if (action.type === 'RESET') { - return; - } - if (action.type === 'IN_PROGRESS') { - return { - status: 'IN_PROGRESS', - options: action.value, - }; - } - if (state?.status === 'IN_PROGRESS' && action.type === 'COMPLETE') { - return { - ...state, - status: 'COMPLETE', - value: action.value, - }; - } - if (action.type === 'AUTO') { - return { - status: 'COMPLETE', - options: action.value.options, - value: action.value.value, - }; - } - - return state; -}; - -const createStore: Store = reducer => { - let currentState: MfaSetupState; - - return { - getState: () => currentState, - dispatch: action => { - currentState = reducer(currentState, action); - }, - }; -}; - -export const mfaSetupStore = createStore(mfaSetupReducer); - -export const resetMfaSetupState = () => { - mfaSetupStore.dispatch({ type: 'RESET' }); -}; diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index aa2775117e2..d43c83d8c11 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -61,7 +61,6 @@ import { import { BigInteger } from './srp/BigInteger'; import { AuthenticationHelper } from './srp/AuthenticationHelper'; import { getUserContextData } from './userContextData'; -import { mfaSetupStore } from './mfaSetupStore'; const USER_ATTRIBUTES = 'userAttributes.'; @@ -150,76 +149,74 @@ export async function handleMFASetupChallenge({ }: HandleAuthChallengeRequest): Promise { const { userPoolId, userPoolClientId } = config; - const mfaSetupState = mfaSetupStore.getState(); - - if (mfaSetupState?.status === 'IN_PROGRESS') { - if ( - (challengeResponse === 'EMAIL' || challengeResponse === 'TOTP') && - mfaSetupState.options.includes(challengeResponse) - ) { - mfaSetupStore.dispatch({ type: 'COMPLETE', value: challengeResponse }); - - return { - ChallengeName: 'MFA_SETUP', - Session: session, - $metadata: {}, - }; - } + if (challengeResponse === 'EMAIL') { + return { + ChallengeName: 'MFA_SETUP', + Session: session, + ChallengeParameters: { + MFAS_CAN_SETUP: '["EMAIL_MFA"]', + }, + $metadata: {}, + }; } - if (mfaSetupState?.status === 'COMPLETE') { - const challengeResponses: Record = { - USERNAME: username, + if (challengeResponse === 'TOTP') { + return { + ChallengeName: 'MFA_SETUP', + Session: session, + ChallengeParameters: { + MFAS_CAN_SETUP: '["SOFTWARE_TOKEN_MFA"]', + }, + $metadata: {}, }; + } - if (mfaSetupState.value === 'EMAIL') { - challengeResponses.EMAIL = challengeResponse; + const isTOTPCode = /^\d+$/.test(challengeResponse.trim()); - const jsonReq: RespondToAuthChallengeCommandInput = { - ChallengeName: 'MFA_SETUP', - ChallengeResponses: challengeResponses, + const challengeResponses: Record = { + USERNAME: username, + }; + + if (isTOTPCode) { + const { Session } = await verifySoftwareToken( + { + region: getRegion(userPoolId), + userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), + }, + { + UserCode: challengeResponse, Session: session, - ClientMetadata: clientMetadata, - ClientId: userPoolClientId, - }; + FriendlyDeviceName: deviceName, + }, + ); - return respondToAuthChallenge({ region: getRegion(userPoolId) }, jsonReq); - } + signInStore.dispatch({ + type: 'SET_SIGN_IN_SESSION', + value: Session, + }); - if (mfaSetupState.value === 'TOTP') { - const { Session } = await verifySoftwareToken( - { - region: getRegion(userPoolId), - userAgentValue: getAuthUserAgentValue(AuthAction.ConfirmSignIn), - }, - { - UserCode: challengeResponse, - Session: session, - FriendlyDeviceName: deviceName, - }, - ); + const jsonReq: RespondToAuthChallengeCommandInput = { + ChallengeName: 'MFA_SETUP', + ChallengeResponses: challengeResponses, + Session, + ClientMetadata: clientMetadata, + ClientId: userPoolClientId, + }; - signInStore.dispatch({ - type: 'SET_SIGN_IN_SESSION', - value: Session, - }); + return respondToAuthChallenge({ region: getRegion(userPoolId) }, jsonReq); + } - const jsonReq: RespondToAuthChallengeCommandInput = { - ChallengeName: 'MFA_SETUP', - ChallengeResponses: challengeResponses, - Session, - ClientMetadata: clientMetadata, - ClientId: userPoolClientId, - }; + challengeResponses.EMAIL = challengeResponse; - return respondToAuthChallenge({ region: getRegion(userPoolId) }, jsonReq); - } - } + const jsonReq: RespondToAuthChallengeCommandInput = { + ChallengeName: 'MFA_SETUP', + ChallengeResponses: challengeResponses, + Session: session, + ClientMetadata: clientMetadata, + ClientId: userPoolClientId, + }; - throw new AuthError({ - name: AuthErrorCodes.SignInException, - message: `Cannot initiate MFA setup from available types: ${mfaSetupState?.options}`, - }); + return respondToAuthChallenge({ region: getRegion(userPoolId) }, jsonReq); } export async function handleSelectMFATypeChallenge({ @@ -732,43 +729,6 @@ export async function getSignInResult(params: { }; case 'MFA_SETUP': { const { signInSession, username } = signInStore.getState(); - const mfaSetupState = mfaSetupStore.getState(); - - if (mfaSetupState?.status === 'COMPLETE') { - if (mfaSetupState.value === 'EMAIL') { - return { - isSignedIn: false, - nextStep: { - signInStep: 'CONTINUE_SIGN_IN_WITH_EMAIL_SETUP', - }, - }; - } - if (mfaSetupState.value === 'TOTP') { - 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), - }, - }; - } - throw new AuthError({ - name: AuthErrorCodes.SignInException, - message: `Cannot initiate MFA setup from available types: ${mfaSetupState.options}`, - }); - } const allowedMfaSetupTypes = getAllowedMfaSetupTypes( challengeParameters.MFAS_CAN_SETUP, @@ -778,11 +738,6 @@ export async function getSignInResult(params: { const isEmailMfaSetupAvailable = allowedMfaSetupTypes.includes('EMAIL'); if (isTotpMfaSetupAvailable && isEmailMfaSetupAvailable) { - mfaSetupStore.dispatch({ - type: 'IN_PROGRESS', - value: allowedMfaSetupTypes, - }); - return { isSignedIn: false, nextStep: { @@ -793,14 +748,6 @@ export async function getSignInResult(params: { } if (isEmailMfaSetupAvailable) { - mfaSetupStore.dispatch({ - type: 'AUTO', - value: { - value: 'EMAIL', - options: allowedMfaSetupTypes, - }, - }); - return { isSignedIn: false, nextStep: { @@ -810,14 +757,6 @@ export async function getSignInResult(params: { } if (isTotpMfaSetupAvailable) { - mfaSetupStore.dispatch({ - type: 'AUTO', - value: { - value: 'TOTP', - options: allowedMfaSetupTypes, - }, - }); - const { Session, SecretCode: secretCode } = await associateSoftwareToken( { region: getRegion(authConfig.userPoolId) }, diff --git a/packages/auth/src/providers/cognito/utils/signInStore.ts b/packages/auth/src/providers/cognito/utils/signInStore.ts index 1fd34b92178..42aed53cea1 100644 --- a/packages/auth/src/providers/cognito/utils/signInStore.ts +++ b/packages/auth/src/providers/cognito/utils/signInStore.ts @@ -3,7 +3,6 @@ import { CognitoAuthSignInDetails } from '../types'; -import { Reducer, Store } from './types'; import { ChallengeName } from './clients/CognitoIdentityProvider/types'; // TODO: replace all of this implementation with state machines @@ -21,6 +20,13 @@ type SignInAction = | { type: 'SET_CHALLENGE_NAME'; value?: ChallengeName } | { type: 'SET_SIGN_IN_SESSION'; value?: string }; +export type Store = (reducer: Reducer) => { + getState(): ReturnType>; + dispatch(action: Action): void; +}; + +export type Reducer = (state: State, action: Action) => State; + const signInReducer: Reducer = (state, action) => { switch (action.type) { case 'SET_SIGN_IN_SESSION': diff --git a/packages/auth/src/providers/cognito/utils/types.ts b/packages/auth/src/providers/cognito/utils/types.ts index 98d3ad82860..e6038366885 100644 --- a/packages/auth/src/providers/cognito/utils/types.ts +++ b/packages/auth/src/providers/cognito/utils/types.ts @@ -140,10 +140,3 @@ function isAuthenticatedWithImplicitOauthFlow( ) { return isAuthenticated(tokens) && !tokens?.refreshToken; } - -export type Store = (reducer: Reducer) => { - getState(): ReturnType>; - dispatch(action: Action): void; -}; - -export type Reducer = (state: State, action: Action) => State;