Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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');
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<RespondToAuthChallengeCommandOutput> =>
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<RespondToAuthChallengeCommandOutput> =>
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<RespondToAuthChallengeCommandOutput> =>
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<RespondToAuthChallengeCommandOutput> => ({
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<RespondToAuthChallengeCommandOutput> =>
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<RespondToAuthChallengeCommandOutput> =>
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<RespondToAuthChallengeCommandOutput> => ({
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<RespondToAuthChallengeCommandOutput> =>
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<RespondToAuthChallengeCommandOutput> =>
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<RespondToAuthChallengeCommandOutput> =>
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');
});
});
26 changes: 26 additions & 0 deletions packages/auth/__tests__/providers/cognito/signInErrorCases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/auth/src/providers/cognito/apis/confirmSignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading