Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): Enable resumable SignIn (#13855) #14074

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
14 changes: 14 additions & 0 deletions .github/integ-config/integ-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,20 @@ tests:
sample_name: [subdomains]
spec: subdomains
browser: [chrome]
- test_name: integ_next_custom_auth
desc: 'Sign-in with Custom Auth flow'
framework: next
category: auth
sample_name: [custom-auth]
spec: custom-auth
browser: *minimal_browser_list
- test_name: integ_next_auth_sign_in_with_sms_mfa
desc: 'Resumable sign in with SMS MFA flow'
framework: next
category: auth
sample_name: [mfa]
spec: sign-in-resumable-mfa
browser: *minimal_browser_list

# DISABLED Angular/Vue tests:
# TODO: delete tests or add custom ui logic to support them.
Expand Down
268 changes: 268 additions & 0 deletions packages/auth/__tests__/providers/cognito/signInResumable.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { Amplify, syncSessionStorage } from '@aws-amplify/core';

import {
setActiveSignInState,
signInStore,
} from '../../../src/client/utils/store/signInStore';
import { cognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider';
import {
ChallengeName,
RespondToAuthChallengeCommandOutput,
} from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types';
import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers';
import { signIn } from '../../../src/providers/cognito';

import { setUpGetConfig } from './testUtils/setUpGetConfig';
import { authAPITestParams } from './testUtils/authApiTestParams';

const signInStoreImplementation = require('../../../src/client/utils/store/signInStore');

jest.mock('@aws-amplify/core/internals/utils');
jest.mock('../../../src/providers/cognito/apis/getCurrentUser');
jest.mock('@aws-amplify/core', () => ({
...(jest.createMockFromModule('@aws-amplify/core') as object),
Amplify: {
getConfig: jest.fn(() => ({})),
ADD_OAUTH_LISTENER: jest.fn(() => ({})),
},
syncSessionStorage: {
setItem: jest.fn((key, value) => {
window.sessionStorage.setItem(key, value);
}),
getItem: jest.fn((key: string) => {
return window.sessionStorage.getItem(key);
}),
removeItem: jest.fn((key: string) => {
window.sessionStorage.removeItem(key);
}),
},
}));

const signInStateKeys: Record<string, string> = {
username: 'CognitoSignInState.username',
challengeName: 'CognitoSignInState.challengeName',
signInSession: 'CognitoSignInState.signInSession',
expiry: 'CognitoSignInState.expiry',
};

const user1: Record<string, string> = {
username: 'joonchoi',
challengeName: 'CUSTOM_CHALLENGE',
signInSession: '888577-ltfgo-42d8-891d-666l858766g7',
expiry: '1234567',
};

const populateValidTestSyncStorage = () => {
syncSessionStorage.setItem(signInStateKeys.username, user1.username);
syncSessionStorage.setItem(
signInStateKeys.signInSession,
user1.signInSession,
);
syncSessionStorage.setItem(
signInStateKeys.challengeName,
user1.challengeName,
);
syncSessionStorage.setItem(
signInStateKeys.expiry,
(new Date().getTime() + 9999999).toString(),
);

signInStore.dispatch({
type: 'SET_INITIAL_STATE',
});
};

const populateInvalidTestSyncStorage = () => {
syncSessionStorage.setItem(signInStateKeys.username, user1.username);
syncSessionStorage.setItem(
signInStateKeys.signInSession,
user1.signInSession,
);
syncSessionStorage.setItem(
signInStateKeys.challengeName,
user1.challengeName,
);
syncSessionStorage.setItem(
signInStateKeys.expiry,
(new Date().getTime() - 99999).toString(),
);

signInStore.dispatch({
type: 'SET_INITIAL_STATE',
});
};

describe('signInStore', () => {
const authConfig = {
Cognito: {
userPoolClientId: '123456-abcde-42d8-891d-666l858766g7',
userPoolId: 'us-west-7_ampjc',
},
};

const session = '1234234232';
const challengeName = 'SMS_MFA';
const { username } = authAPITestParams.user1;
const { password } = authAPITestParams.user1;

beforeEach(() => {
cognitoUserPoolsTokenProvider.setAuthConfig(authConfig);
});

beforeAll(() => {
setUpGetConfig(Amplify);
});

afterEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
jest.restoreAllMocks();
});

test('LocalSignInState is empty after initialization', async () => {
const localSignInState = signInStore.getState();

expect(localSignInState).toEqual({
challengeName: undefined,
signInSession: undefined,
username: undefined,
});
signInStore.dispatch({ type: 'RESET_STATE' });
});

test('State is set after calling setActiveSignInState', async () => {
const persistSignInStateSpy = jest.spyOn(
signInStoreImplementation,
'persistSignInState',
);
setActiveSignInState(user1);
const localSignInState = signInStore.getState();

expect(localSignInState).toEqual(user1);
expect(persistSignInStateSpy).toHaveBeenCalledTimes(1);
expect(persistSignInStateSpy).toHaveBeenCalledWith(user1);
signInStore.dispatch({ type: 'RESET_STATE' });
});

test('State is updated after calling SignIn', async () => {
const handleUserSRPAuthflowSpy = jest
.spyOn(signInHelpers, 'handleUserSRPAuthFlow')
.mockImplementationOnce(
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
ChallengeName: challengeName,
Session: session,
$metadata: {},
ChallengeParameters: {
CODE_DELIVERY_DELIVERY_MEDIUM: 'SMS',
CODE_DELIVERY_DESTINATION: '*******9878',
},
}),
);

await signIn({
username,
password,
});
const newLocalSignInState = signInStore.getState();

expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1);
expect(newLocalSignInState).toEqual({
challengeName,
signInSession: session,
username,
signInDetails: {
loginId: username,
authFlowType: 'USER_SRP_AUTH',
},
});
handleUserSRPAuthflowSpy.mockClear();
});

test('The stored sign-in state should be rehydrated if the sign-in session is still valid.', () => {
populateValidTestSyncStorage();

const localSignInState = signInStore.getState();

expect(localSignInState).toEqual({
username: user1.username,
challengeName: user1.challengeName,
signInSession: user1.signInSession,
});
signInStore.dispatch({ type: 'RESET_STATE' });
});

test('sign-in store should return undefined state when the sign-in session is expired', async () => {
populateInvalidTestSyncStorage();

const localSignInState = signInStore.getState();

expect(localSignInState).toEqual({
username: undefined,
challengeName: undefined,
signInSession: undefined,
});
signInStore.dispatch({ type: 'RESET_STATE' });
});

test('State SignInSession is updated after dispatching custom session value', () => {
const persistSignInStateSpy = jest.spyOn(
signInStoreImplementation,
'persistSignInState',
);
const newSignInSessionID = '135790-dodge-2468-9aaa-kersh23lad00';

populateValidTestSyncStorage();

const localSignInState = signInStore.getState();
expect(localSignInState).toEqual({
username: user1.username,
challengeName: user1.challengeName,
signInSession: user1.signInSession,
});

signInStore.dispatch({
type: 'SET_SIGN_IN_SESSION',
value: newSignInSessionID,
});

expect(persistSignInStateSpy).toHaveBeenCalledTimes(1);
expect(persistSignInStateSpy).toHaveBeenCalledWith({
signInSession: newSignInSessionID,
});
const newLocalSignInState = signInStore.getState();
expect(newLocalSignInState).toEqual({
username: user1.username,
challengeName: user1.challengeName,
signInSession: newSignInSessionID,
});
});

test('State Challenge Name is updated after dispatching custom challenge name', () => {
const newChallengeName = 'RANDOM_CHALLENGE' as ChallengeName;

populateValidTestSyncStorage();

const localSignInState = signInStore.getState();
expect(localSignInState).toEqual({
username: user1.username,
challengeName: user1.challengeName,
signInSession: user1.signInSession,
});

signInStore.dispatch({
type: 'SET_CHALLENGE_NAME',
value: newChallengeName,
});

const newLocalSignInState = signInStore.getState();
expect(newLocalSignInState).toEqual({
username: user1.username,
challengeName: newChallengeName,
signInSession: user1.signInSession,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Amplify } from '@aws-amplify/core';

import { getCurrentUser, signIn } from '../../../src/providers/cognito';
import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers';
import { signInStore } from '../../../src/client/utils/store';
import { signInStore } from '../../../src/client/utils/store/signInStore';
import { cognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider';
import { RespondToAuthChallengeCommandOutput } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types';

Expand All @@ -30,6 +30,7 @@ describe('local sign-in state management tests', () => {

beforeEach(() => {
cognitoUserPoolsTokenProvider.setAuthConfig(authConfig);
signInStore.dispatch({ type: 'RESET_STATE' });
});

test('local state management should return state after signIn returns a ChallengeName', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,20 @@ jest.mock('@aws-amplify/core', () => {
getConfig: jest.fn(() => mockAuthConfigWithOAuth),
[ACTUAL_ADD_OAUTH_LISTENER]: jest.fn(),
},
ConsoleLogger: jest.fn(),
ConsoleLogger: jest.fn().mockImplementation(() => {
return { warn: jest.fn() };
}),
syncSessionStorage: {
setItem: jest.fn((key, value) => {
window.sessionStorage.setItem(key, value);
}),
getItem: jest.fn((key: string) => {
return window.sessionStorage.getItem(key);
}),
removeItem: jest.fn((key: string) => {
window.sessionStorage.removeItem(key);
}),
},
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ import {
getNewDeviceMetadata,
getSignInResult,
} from '../../../providers/cognito/utils/signInHelpers';
import {
cleanActiveSignInState,
setActiveSignInState,
signInStore,
} from '../../../client/utils/store';
import { setActiveSignInState, signInStore } from '../../../client/utils/store';
import { AuthSignInOutput } from '../../../types';
import { getAuthUserAgentValue } from '../../../utils';
import { getPasskey } from '../../utils/passkey';
Expand Down Expand Up @@ -106,7 +102,7 @@ export async function handleWebAuthnSignInResult(
}),
signInDetails,
});
cleanActiveSignInState();
signInStore.dispatch({ type: 'RESET_STATE' });
jjarvisp marked this conversation as resolved.
Show resolved Hide resolved
await dispatchSignedInHubEvent();

return {
Expand Down
Loading
Loading