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 #13483

Merged
merged 55 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
e78c8e4
Resumable Sign In
Jun 7, 2024
49bb952
Add clear after rehydration
Jun 7, 2024
611357a
Synchronous Session Storage implementation
Jun 11, 2024
8d3944b
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Jun 11, 2024
814ccf3
Fix error dependency path
Jun 12, 2024
a413779
Modify init sequence
Jun 12, 2024
4532bc8
Bug fix
Jun 12, 2024
2dd0fd9
Move SyncSessionStorage to core
Jun 13, 2024
a54055e
Fix defaulting state
Jun 14, 2024
3816f2f
Fix calling wrong method
Jun 15, 2024
20cd812
Add Unittest for SyncSessionstorage
Jun 15, 2024
5f99aad
Adjust bundle size, fix exports
Jun 15, 2024
f040d10
Adjust bundle size, fix typo
Jun 15, 2024
c4642cf
Loosen jest cov for src/utils
Jun 15, 2024
8abe4d4
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Jun 15, 2024
8237695
ResumableSignIn Unit Tests
Jun 20, 2024
b6d666e
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Jun 20, 2024
e5a5b4d
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Jun 21, 2024
bf370d2
Modified signInStore implementation
Jun 24, 2024
8077bb4
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Jun 24, 2024
b91fec6
Complemented additional behaviors to address potential discrepancy du…
Jul 2, 2024
be3db29
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Jul 10, 2024
9f6c933
Modified State persistence behavior
Jul 10, 2024
82316a5
Update params
Jul 10, 2024
2c7600a
Add mock implementation to resolve test issue
Jul 10, 2024
08419ba
Changed expiration check logic
Jul 10, 2024
7e91d0f
Polish persistSignInState()
Jul 11, 2024
f22d805
Merge branch 'aws-amplify:main' into joonwonc/auth-resumable-signin
joon-won Aug 1, 2024
134e748
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Aug 9, 2024
e0eed74
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Aug 15, 2024
9a0d6d1
Adjust bundle size
Aug 15, 2024
363635d
Merge branch 'aws-amplify:main' into joonwonc/auth-resumable-signin
joon-won Aug 21, 2024
247ccc9
Add persisting action for session
Aug 21, 2024
539688b
Merge branch 'aws-amplify:main' into joonwonc/auth-resumable-signin
joon-won Aug 22, 2024
dea838e
Add spyOn checkpoints
Aug 22, 2024
deb0d9d
enable integ
Aug 23, 2024
aae163e
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Aug 23, 2024
dbe8d02
Revert jest config for aws-amplify package test
Aug 23, 2024
7d0527c
revert workflow
Aug 23, 2024
c71e4ea
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Aug 26, 2024
efb2331
Remove redundant features, improve efficiency
Aug 27, 2024
4899023
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Aug 28, 2024
c4b043a
Adjust bundle size limit
Aug 28, 2024
3e54290
Remove redundancy, fix annotation
Aug 28, 2024
9f488c5
Replace cleanActiveSignInState()
Aug 28, 2024
6168bcb
Enable CI for Resumable SMS MFA
Aug 29, 2024
ed930f6
Merge branch 'joonwonc/auth-resumable-smsmfa' into joonwonc/auth-resu…
joon-won Aug 29, 2024
d19f72d
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Aug 30, 2024
1f120ab
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Sep 12, 2024
92ce212
Modify dependency to foundations
Sep 12, 2024
bd5b099
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Sep 13, 2024
224f148
Update Integ
Sep 13, 2024
0b0c518
Revert
Sep 13, 2024
5d4f6b5
Merge branch 'main' into joonwonc/auth-resumable-signin
joon-won Sep 20, 2024
a5326cc
Merge branch 'aws-amplify:main' into joonwonc/auth-resumable-signin
joon-won Sep 23, 2024
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: [chrome]

# 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/providers/cognito/utils/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/providers/cognito/utils/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> = {
joon-won marked this conversation as resolved.
Show resolved Hide resolved
username: 'CognitoSignInState.username',
challengeName: 'CognitoSignInState.challengeName',
signInSession: 'CognitoSignInState.signInSession',
expiry: 'CognitoSignInState.expiry',
};

const user1: Record<string, string> = {
joon-won marked this conversation as resolved.
Show resolved Hide resolved
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);
joon-won marked this conversation as resolved.
Show resolved Hide resolved
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({
joon-won marked this conversation as resolved.
Show resolved Hide resolved
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,
});
joon-won marked this conversation as resolved.
Show resolved Hide resolved
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,
});
joon-won marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -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
9 changes: 3 additions & 6 deletions packages/auth/src/providers/cognito/apis/confirmSignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ import {
VerifySoftwareTokenException,
} from '../types/errors';
import { ConfirmSignInInput, ConfirmSignInOutput } from '../types';
import {
cleanActiveSignInState,
setActiveSignInState,
signInStore,
} from '../utils/signInStore';
import { setActiveSignInState, signInStore } from '../utils/signInStore';
import { AuthError } from '../../../errors/AuthError';
import {
getNewDeviceMetadata,
Expand Down Expand Up @@ -109,7 +105,8 @@ export async function confirmSignIn(
});

if (AuthenticationResult) {
cleanActiveSignInState();
signInStore.dispatch({ type: 'RESET_STATE' });

await cacheCognitoTokens({
username,
...AuthenticationResult,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ import {
SignInWithCustomAuthInput,
SignInWithCustomAuthOutput,
} from '../types';
import {
cleanActiveSignInState,
setActiveSignInState,
} from '../utils/signInStore';
import { setActiveSignInState, signInStore } from '../utils/signInStore';
import { cacheCognitoTokens } from '../tokenProvider/cacheTokens';
import {
ChallengeName,
Expand Down Expand Up @@ -84,7 +81,7 @@ export async function signInWithCustomAuth(
signInDetails,
});
if (AuthenticationResult) {
cleanActiveSignInState();
signInStore.dispatch({ type: 'RESET_STATE' });

await cacheCognitoTokens({
username: activeUsername,
Expand All @@ -111,7 +108,7 @@ export async function signInWithCustomAuth(
challengeParameters: retiredChallengeParameters as ChallengeParameters,
});
} catch (error) {
cleanActiveSignInState();
signInStore.dispatch({ type: 'RESET_STATE' });
assertServiceError(error);
const result = getSignInResultFromError(error.name);
if (result) return result;
Expand Down
Loading
Loading