Skip to content

Commit

Permalink
feat(auth): Enable resumable SignIn (#13483)
Browse files Browse the repository at this point in the history
* Auth Resumable Sign In

---------

Co-authored-by: JoonWon Choi <joonwonc@amazon.com>
  • Loading branch information
joon-won and JoonWon Choi authored Sep 23, 2024
1 parent 63bceab commit f3421f1
Show file tree
Hide file tree
Showing 18 changed files with 577 additions and 76 deletions.
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> = {
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 @@ -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

0 comments on commit f3421f1

Please sign in to comment.