Skip to content

Commit f3421f1

Browse files
joon-wonJoonWon Choi
andauthored
feat(auth): Enable resumable SignIn (#13483)
* Auth Resumable Sign In --------- Co-authored-by: JoonWon Choi <[email protected]>
1 parent 63bceab commit f3421f1

18 files changed

+577
-76
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: [chrome]
612626

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/providers/cognito/apis/confirmSignIn.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ import {
1010
VerifySoftwareTokenException,
1111
} from '../types/errors';
1212
import { ConfirmSignInInput, ConfirmSignInOutput } from '../types';
13-
import {
14-
cleanActiveSignInState,
15-
setActiveSignInState,
16-
signInStore,
17-
} from '../utils/signInStore';
13+
import { setActiveSignInState, signInStore } from '../utils/signInStore';
1814
import { AuthError } from '../../../errors/AuthError';
1915
import {
2016
getNewDeviceMetadata,
@@ -109,7 +105,8 @@ export async function confirmSignIn(
109105
});
110106

111107
if (AuthenticationResult) {
112-
cleanActiveSignInState();
108+
signInStore.dispatch({ type: 'RESET_STATE' });
109+
113110
await cacheCognitoTokens({
114111
username,
115112
...AuthenticationResult,

packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ import {
2121
SignInWithCustomAuthInput,
2222
SignInWithCustomAuthOutput,
2323
} from '../types';
24-
import {
25-
cleanActiveSignInState,
26-
setActiveSignInState,
27-
} from '../utils/signInStore';
24+
import { setActiveSignInState, signInStore } from '../utils/signInStore';
2825
import { cacheCognitoTokens } from '../tokenProvider/cacheTokens';
2926
import {
3027
ChallengeName,
@@ -84,7 +81,7 @@ export async function signInWithCustomAuth(
8481
signInDetails,
8582
});
8683
if (AuthenticationResult) {
87-
cleanActiveSignInState();
84+
signInStore.dispatch({ type: 'RESET_STATE' });
8885

8986
await cacheCognitoTokens({
9087
username: activeUsername,
@@ -111,7 +108,7 @@ export async function signInWithCustomAuth(
111108
challengeParameters: retiredChallengeParameters as ChallengeParameters,
112109
});
113110
} catch (error) {
114-
cleanActiveSignInState();
111+
signInStore.dispatch({ type: 'RESET_STATE' });
115112
assertServiceError(error);
116113
const result = getSignInResultFromError(error.name);
117114
if (result) return result;

0 commit comments

Comments
 (0)