Skip to content

Commit fe315be

Browse files
joon-woncshfang
andauthored
feat(auth): Enable resumable SignIn (#13855) (#14074)
* feat(auth): Enable resumable SignIn --------- Co-authored-by: Chris F <[email protected]>
1 parent 31c001d commit fe315be

File tree

20 files changed

+579
-74
lines changed

20 files changed

+579
-74
lines changed

.github/integ-config/integ-all.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,20 @@ tests:
609609
sample_name: [subdomains]
610610
spec: subdomains
611611
browser: [chrome]
612+
- test_name: integ_next_custom_auth
613+
desc: 'Sign-in with Custom Auth flow'
614+
framework: next
615+
category: auth
616+
sample_name: [custom-auth]
617+
spec: custom-auth
618+
browser: *minimal_browser_list
619+
- test_name: integ_next_auth_sign_in_with_sms_mfa
620+
desc: 'Resumable sign in with SMS MFA flow'
621+
framework: next
622+
category: auth
623+
sample_name: [mfa]
624+
spec: sign-in-resumable-mfa
625+
browser: *minimal_browser_list
612626

613627
# DISABLED Angular/Vue tests:
614628
# TODO: delete tests or add custom ui logic to support them.
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { Amplify, syncSessionStorage } from '@aws-amplify/core';
4+
5+
import {
6+
resetActiveSignInState,
7+
setActiveSignInState,
8+
signInStore,
9+
} from '../../../src/client/utils/store/signInStore';
10+
import { cognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider';
11+
import {
12+
ChallengeName,
13+
RespondToAuthChallengeCommandOutput,
14+
} from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types';
15+
import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers';
16+
import { signIn } from '../../../src/providers/cognito';
17+
18+
import { setUpGetConfig } from './testUtils/setUpGetConfig';
19+
import { authAPITestParams } from './testUtils/authApiTestParams';
20+
21+
const signInStoreImplementation = require('../../../src/client/utils/store/signInStore');
22+
23+
jest.mock('@aws-amplify/core/internals/utils');
24+
jest.mock('../../../src/providers/cognito/apis/getCurrentUser');
25+
jest.mock('@aws-amplify/core', () => ({
26+
...(jest.createMockFromModule('@aws-amplify/core') as object),
27+
Amplify: {
28+
getConfig: jest.fn(() => ({})),
29+
ADD_OAUTH_LISTENER: jest.fn(() => ({})),
30+
},
31+
syncSessionStorage: {
32+
setItem: jest.fn((key, value) => {
33+
window.sessionStorage.setItem(key, value);
34+
}),
35+
getItem: jest.fn((key: string) => {
36+
return window.sessionStorage.getItem(key);
37+
}),
38+
removeItem: jest.fn((key: string) => {
39+
window.sessionStorage.removeItem(key);
40+
}),
41+
},
42+
}));
43+
44+
const signInStateKeys: Record<string, string> = {
45+
username: 'CognitoSignInState.username',
46+
challengeName: 'CognitoSignInState.challengeName',
47+
signInSession: 'CognitoSignInState.signInSession',
48+
expiry: 'CognitoSignInState.expiry',
49+
};
50+
51+
const user1: Record<string, string> = {
52+
username: 'joonchoi',
53+
challengeName: 'CUSTOM_CHALLENGE',
54+
signInSession: '888577-ltfgo-42d8-891d-666l858766g7',
55+
expiry: '1234567',
56+
};
57+
58+
const populateValidTestSyncStorage = () => {
59+
syncSessionStorage.setItem(signInStateKeys.username, user1.username);
60+
syncSessionStorage.setItem(
61+
signInStateKeys.signInSession,
62+
user1.signInSession,
63+
);
64+
syncSessionStorage.setItem(
65+
signInStateKeys.challengeName,
66+
user1.challengeName,
67+
);
68+
syncSessionStorage.setItem(
69+
signInStateKeys.expiry,
70+
(new Date().getTime() + 9999999).toString(),
71+
);
72+
73+
signInStore.dispatch({
74+
type: 'SET_INITIAL_STATE',
75+
});
76+
};
77+
78+
const populateInvalidTestSyncStorage = () => {
79+
syncSessionStorage.setItem(signInStateKeys.username, user1.username);
80+
syncSessionStorage.setItem(
81+
signInStateKeys.signInSession,
82+
user1.signInSession,
83+
);
84+
syncSessionStorage.setItem(
85+
signInStateKeys.challengeName,
86+
user1.challengeName,
87+
);
88+
syncSessionStorage.setItem(
89+
signInStateKeys.expiry,
90+
(new Date().getTime() - 99999).toString(),
91+
);
92+
93+
signInStore.dispatch({
94+
type: 'SET_INITIAL_STATE',
95+
});
96+
};
97+
98+
describe('signInStore', () => {
99+
const authConfig = {
100+
Cognito: {
101+
userPoolClientId: '123456-abcde-42d8-891d-666l858766g7',
102+
userPoolId: 'us-west-7_ampjc',
103+
},
104+
};
105+
106+
const session = '1234234232';
107+
const challengeName = 'SMS_MFA';
108+
const { username } = authAPITestParams.user1;
109+
const { password } = authAPITestParams.user1;
110+
111+
beforeEach(() => {
112+
cognitoUserPoolsTokenProvider.setAuthConfig(authConfig);
113+
});
114+
115+
beforeAll(() => {
116+
setUpGetConfig(Amplify);
117+
});
118+
119+
afterEach(() => {
120+
jest.clearAllMocks();
121+
});
122+
123+
afterAll(() => {
124+
jest.restoreAllMocks();
125+
});
126+
127+
test('LocalSignInState is empty after initialization', async () => {
128+
const localSignInState = signInStore.getState();
129+
130+
expect(localSignInState).toEqual({
131+
challengeName: undefined,
132+
signInSession: undefined,
133+
username: undefined,
134+
});
135+
resetActiveSignInState();
136+
});
137+
138+
test('State is set after calling setActiveSignInState', async () => {
139+
const persistSignInStateSpy = jest.spyOn(
140+
signInStoreImplementation,
141+
'persistSignInState',
142+
);
143+
setActiveSignInState(user1);
144+
const localSignInState = signInStore.getState();
145+
146+
expect(localSignInState).toEqual(user1);
147+
expect(persistSignInStateSpy).toHaveBeenCalledTimes(1);
148+
expect(persistSignInStateSpy).toHaveBeenCalledWith(user1);
149+
resetActiveSignInState();
150+
});
151+
152+
test('State is updated after calling SignIn', async () => {
153+
const handleUserSRPAuthflowSpy = jest
154+
.spyOn(signInHelpers, 'handleUserSRPAuthFlow')
155+
.mockImplementationOnce(
156+
async (): Promise<RespondToAuthChallengeCommandOutput> => ({
157+
ChallengeName: challengeName,
158+
Session: session,
159+
$metadata: {},
160+
ChallengeParameters: {
161+
CODE_DELIVERY_DELIVERY_MEDIUM: 'SMS',
162+
CODE_DELIVERY_DESTINATION: '*******9878',
163+
},
164+
}),
165+
);
166+
167+
await signIn({
168+
username,
169+
password,
170+
});
171+
const newLocalSignInState = signInStore.getState();
172+
173+
expect(handleUserSRPAuthflowSpy).toHaveBeenCalledTimes(1);
174+
expect(newLocalSignInState).toEqual({
175+
challengeName,
176+
signInSession: session,
177+
username,
178+
signInDetails: {
179+
loginId: username,
180+
authFlowType: 'USER_SRP_AUTH',
181+
},
182+
});
183+
handleUserSRPAuthflowSpy.mockClear();
184+
});
185+
186+
test('The stored sign-in state should be rehydrated if the sign-in session is still valid.', () => {
187+
populateValidTestSyncStorage();
188+
189+
const localSignInState = signInStore.getState();
190+
191+
expect(localSignInState).toEqual({
192+
username: user1.username,
193+
challengeName: user1.challengeName,
194+
signInSession: user1.signInSession,
195+
});
196+
resetActiveSignInState();
197+
});
198+
199+
test('sign-in store should return undefined state when the sign-in session is expired', async () => {
200+
populateInvalidTestSyncStorage();
201+
202+
const localSignInState = signInStore.getState();
203+
204+
expect(localSignInState).toEqual({
205+
username: undefined,
206+
challengeName: undefined,
207+
signInSession: undefined,
208+
});
209+
resetActiveSignInState();
210+
});
211+
212+
test('State SignInSession is updated after dispatching custom session value', () => {
213+
const persistSignInStateSpy = jest.spyOn(
214+
signInStoreImplementation,
215+
'persistSignInState',
216+
);
217+
const newSignInSessionID = '135790-dodge-2468-9aaa-kersh23lad00';
218+
219+
populateValidTestSyncStorage();
220+
221+
const localSignInState = signInStore.getState();
222+
expect(localSignInState).toEqual({
223+
username: user1.username,
224+
challengeName: user1.challengeName,
225+
signInSession: user1.signInSession,
226+
});
227+
228+
signInStore.dispatch({
229+
type: 'SET_SIGN_IN_SESSION',
230+
value: newSignInSessionID,
231+
});
232+
233+
expect(persistSignInStateSpy).toHaveBeenCalledTimes(1);
234+
expect(persistSignInStateSpy).toHaveBeenCalledWith({
235+
signInSession: newSignInSessionID,
236+
});
237+
const newLocalSignInState = signInStore.getState();
238+
expect(newLocalSignInState).toEqual({
239+
username: user1.username,
240+
challengeName: user1.challengeName,
241+
signInSession: newSignInSessionID,
242+
});
243+
});
244+
245+
test('State Challenge Name is updated after dispatching custom challenge name', () => {
246+
const newChallengeName = 'RANDOM_CHALLENGE' as ChallengeName;
247+
248+
populateValidTestSyncStorage();
249+
250+
const localSignInState = signInStore.getState();
251+
expect(localSignInState).toEqual({
252+
username: user1.username,
253+
challengeName: user1.challengeName,
254+
signInSession: user1.signInSession,
255+
});
256+
257+
signInStore.dispatch({
258+
type: 'SET_CHALLENGE_NAME',
259+
value: newChallengeName,
260+
});
261+
262+
const newLocalSignInState = signInStore.getState();
263+
expect(newLocalSignInState).toEqual({
264+
username: user1.username,
265+
challengeName: newChallengeName,
266+
signInSession: user1.signInSession,
267+
});
268+
});
269+
});

packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Amplify } from '@aws-amplify/core';
55

66
import { getCurrentUser, signIn } from '../../../src/providers/cognito';
77
import * as signInHelpers from '../../../src/providers/cognito/utils/signInHelpers';
8-
import { signInStore } from '../../../src/client/utils/store';
8+
import { signInStore } from '../../../src/client/utils/store/signInStore';
99
import { cognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider';
1010
import { RespondToAuthChallengeCommandOutput } from '../../../src/foundation/factories/serviceClients/cognitoIdentityProvider/types';
1111

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

3131
beforeEach(() => {
3232
cognitoUserPoolsTokenProvider.setAuthConfig(authConfig);
33+
signInStore.dispatch({ type: 'RESET_STATE' });
3334
});
3435

3536
test('local state management should return state after signIn returns a ChallengeName', async () => {

packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,20 @@ jest.mock('@aws-amplify/core', () => {
4242
getConfig: jest.fn(() => mockAuthConfigWithOAuth),
4343
[ACTUAL_ADD_OAUTH_LISTENER]: jest.fn(),
4444
},
45-
ConsoleLogger: jest.fn(),
45+
ConsoleLogger: jest.fn().mockImplementation(() => {
46+
return { warn: jest.fn() };
47+
}),
48+
syncSessionStorage: {
49+
setItem: jest.fn((key, value) => {
50+
window.sessionStorage.setItem(key, value);
51+
}),
52+
getItem: jest.fn((key: string) => {
53+
return window.sessionStorage.getItem(key);
54+
}),
55+
removeItem: jest.fn((key: string) => {
56+
window.sessionStorage.removeItem(key);
57+
}),
58+
},
4659
};
4760
});
4861

packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@ import {
2121
getNewDeviceMetadata,
2222
getSignInResult,
2323
} from '../../../providers/cognito/utils/signInHelpers';
24-
import {
25-
cleanActiveSignInState,
26-
setActiveSignInState,
27-
signInStore,
28-
} from '../../../client/utils/store';
24+
import { setActiveSignInState, signInStore } from '../../../client/utils/store';
2925
import { AuthSignInOutput } from '../../../types';
3026
import { getAuthUserAgentValue } from '../../../utils';
3127
import { getPasskey } from '../../utils/passkey';
@@ -106,7 +102,7 @@ export async function handleWebAuthnSignInResult(
106102
}),
107103
signInDetails,
108104
});
109-
cleanActiveSignInState();
105+
signInStore.dispatch({ type: 'RESET_STATE' });
110106
await dispatchSignedInHubEvent();
111107

112108
return {

0 commit comments

Comments
 (0)