diff --git a/.github/integ-config/integ-all.yml b/.github/integ-config/integ-all.yml index 4759997ee29..047a7385eb7 100644 --- a/.github/integ-config/integ-all.yml +++ b/.github/integ-config/integ-all.yml @@ -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. diff --git a/packages/auth/__tests__/providers/cognito/signInResumable.test.ts b/packages/auth/__tests__/providers/cognito/signInResumable.test.ts new file mode 100644 index 00000000000..7bc3a8d324a --- /dev/null +++ b/packages/auth/__tests__/providers/cognito/signInResumable.test.ts @@ -0,0 +1,269 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { Amplify, syncSessionStorage } from '@aws-amplify/core'; + +import { + resetActiveSignInState, + 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 = { + username: 'CognitoSignInState.username', + challengeName: 'CognitoSignInState.challengeName', + signInSession: 'CognitoSignInState.signInSession', + expiry: 'CognitoSignInState.expiry', +}; + +const user1: Record = { + 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, + }); + resetActiveSignInState(); + }); + + 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); + resetActiveSignInState(); + }); + + test('State is updated after calling SignIn', async () => { + const handleUserSRPAuthflowSpy = jest + .spyOn(signInHelpers, 'handleUserSRPAuthFlow') + .mockImplementationOnce( + async (): Promise => ({ + 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, + }); + resetActiveSignInState(); + }); + + 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, + }); + resetActiveSignInState(); + }); + + 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, + }); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts b/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts index 73e3cdc6eea..bf0735f8f07 100644 --- a/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInStateManagement.test.ts @@ -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'; @@ -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 () => { diff --git a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts index 8f91323319f..486d4fd9a81 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithRedirect.test.ts @@ -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); + }), + }, }; }); diff --git a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts index 2e67a52a5ab..ee3bc6e6b6c 100644 --- a/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts +++ b/packages/auth/src/client/flows/userAuth/handleWebAuthnSignInResult.ts @@ -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'; @@ -106,7 +102,7 @@ export async function handleWebAuthnSignInResult( }), signInDetails, }); - cleanActiveSignInState(); + signInStore.dispatch({ type: 'RESET_STATE' }); await dispatchSignedInHubEvent(); return { diff --git a/packages/auth/src/client/utils/store/signInStore.ts b/packages/auth/src/client/utils/store/signInStore.ts index 94311ce2b74..80b27f2a1c2 100644 --- a/packages/auth/src/client/utils/store/signInStore.ts +++ b/packages/auth/src/client/utils/store/signInStore.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { syncSessionStorage } from '@aws-amplify/core'; + import { CognitoAuthSignInDetails } from '../../../providers/cognito/types'; import { ChallengeName } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; @@ -19,46 +21,116 @@ type SignInAction = | { type: 'SET_SIGN_IN_STATE'; value: SignInState } | { type: 'SET_USERNAME'; value?: string } | { type: 'SET_CHALLENGE_NAME'; value?: ChallengeName } - | { type: 'SET_SIGN_IN_SESSION'; value?: string }; + | { type: 'SET_SIGN_IN_SESSION'; value?: string } + | { type: 'RESET_STATE' }; + +// Minutes until stored session invalidates +const MS_TO_EXPIRY = 3 * 60 * 1000; // 3 mins +const TGT_STATE = 'CognitoSignInState'; +const SIGN_IN_STATE_KEYS = { + username: `${TGT_STATE}.username`, + challengeName: `${TGT_STATE}.challengeName`, + signInSession: `${TGT_STATE}.signInSession`, + expiry: `${TGT_STATE}.expiry`, +}; const signInReducer: Reducer = (state, action) => { switch (action.type) { case 'SET_SIGN_IN_SESSION': + persistSignInState({ signInSession: action.value }); + return { ...state, signInSession: action.value, }; + case 'SET_SIGN_IN_STATE': + persistSignInState(action.value); + return { ...action.value, }; + case 'SET_CHALLENGE_NAME': + persistSignInState({ challengeName: action.value }); + return { ...state, challengeName: action.value, }; + case 'SET_USERNAME': + persistSignInState({ username: action.value }); + return { ...state, username: action.value, }; + case 'SET_INITIAL_STATE': - return defaultState(); + return getInitialState(); + + case 'RESET_STATE': + clearPersistedSignInState(); + + return getDefaultState(); + + // this state is never reachable default: return state; } }; -function defaultState(): SignInState { +const isExpired = (expiryDate: string | null): boolean => { + const expiryTimestamp = Number(expiryDate); + const currentTimestamp = Date.now(); + + return expiryTimestamp <= currentTimestamp; +}; + +export const resetActiveSignInState = () => { + signInStore.dispatch({ type: 'RESET_STATE' }); +}; + +const clearPersistedSignInState = () => { + for (const stateKey of Object.values(SIGN_IN_STATE_KEYS)) { + syncSessionStorage.removeItem(stateKey); + } +}; + +const getDefaultState = (): SignInState => ({ + username: undefined, + challengeName: undefined, + signInSession: undefined, +}); + +// Hydrate signInStore from syncSessionStorage +const getInitialState = (): SignInState => { + const expiry = syncSessionStorage.getItem(SIGN_IN_STATE_KEYS.expiry); + + if (!expiry || isExpired(expiry)) { + clearPersistedSignInState(); + + return getDefaultState(); + } + const username = + syncSessionStorage.getItem(SIGN_IN_STATE_KEYS.username) ?? undefined; + + const challengeName = (syncSessionStorage.getItem( + SIGN_IN_STATE_KEYS.challengeName, + ) ?? undefined) as ChallengeName; + const signInSession = + syncSessionStorage.getItem(SIGN_IN_STATE_KEYS.signInSession) ?? undefined; + return { - username: undefined, - challengeName: undefined, - signInSession: undefined, + username, + challengeName, + signInSession, }; -} +}; const createStore: Store = reducer => { - let currentState = reducer(defaultState(), { type: 'SET_INITIAL_STATE' }); + let currentState = reducer(getDefaultState(), { type: 'SET_INITIAL_STATE' }); return { getState: () => currentState, @@ -77,6 +149,23 @@ export function setActiveSignInState(state: SignInState): void { }); } -export function cleanActiveSignInState(): void { - signInStore.dispatch({ type: 'SET_INITIAL_STATE' }); -} +// Save local state into Session Storage +export const persistSignInState = ({ + challengeName, + signInSession, + username, +}: SignInState) => { + username && syncSessionStorage.setItem(SIGN_IN_STATE_KEYS.username, username); + challengeName && + syncSessionStorage.setItem(SIGN_IN_STATE_KEYS.challengeName, challengeName); + + if (signInSession) { + syncSessionStorage.setItem(SIGN_IN_STATE_KEYS.signInSession, signInSession); + + // Updates expiry when session is passed + syncSessionStorage.setItem( + SIGN_IN_STATE_KEYS.expiry, + String(Date.now() + MS_TO_EXPIRY), + ); + } +}; diff --git a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts index ae62578be5e..9a9af8e75b5 100644 --- a/packages/auth/src/providers/cognito/apis/confirmSignIn.ts +++ b/packages/auth/src/providers/cognito/apis/confirmSignIn.ts @@ -11,7 +11,7 @@ import { } from '../types/errors'; import { ConfirmSignInInput, ConfirmSignInOutput } from '../types'; import { - cleanActiveSignInState, + resetActiveSignInState, setActiveSignInState, signInStore, } from '../../../client/utils/store'; @@ -120,7 +120,7 @@ export async function confirmSignIn( }), signInDetails, }); - cleanActiveSignInState(); + resetActiveSignInState(); await dispatchSignedInHubEvent(); diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts index 5911266475b..3ee2de1302c 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomAuth.ts @@ -22,9 +22,9 @@ import { SignInWithCustomAuthOutput, } from '../types'; import { - cleanActiveSignInState, + resetActiveSignInState, setActiveSignInState, -} from '../../../client/utils/store'; +} from '../../../client/utils/store/signInStore'; import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { ChallengeName, @@ -95,7 +95,7 @@ export async function signInWithCustomAuth( }), signInDetails, }); - cleanActiveSignInState(); + resetActiveSignInState(); await dispatchSignedInHubEvent(); @@ -110,7 +110,7 @@ export async function signInWithCustomAuth( challengeParameters: retiredChallengeParameters as ChallengeParameters, }); } catch (error) { - cleanActiveSignInState(); + resetActiveSignInState(); assertServiceError(error); const result = getSignInResultFromError(error.name); if (result) return result; diff --git a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts index 4966cfaa9fa..35eb7f29419 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithCustomSRPAuth.ts @@ -24,9 +24,9 @@ import { SignInWithCustomSRPAuthOutput, } from '../types'; import { - cleanActiveSignInState, + resetActiveSignInState, setActiveSignInState, -} from '../../../client/utils/store'; +} from '../../../client/utils/store/signInStore'; import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { ChallengeName, @@ -100,7 +100,7 @@ export async function signInWithCustomSRPAuth( }), signInDetails, }); - cleanActiveSignInState(); + resetActiveSignInState(); await dispatchSignedInHubEvent(); @@ -115,7 +115,7 @@ export async function signInWithCustomSRPAuth( challengeParameters: handledChallengeParameters as ChallengeParameters, }); } catch (error) { - cleanActiveSignInState(); + resetActiveSignInState(); assertServiceError(error); const result = getSignInResultFromError(error.name); if (result) return result; diff --git a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts index aa92e3e6012..05c79cf35a0 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithSRP.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithSRP.ts @@ -28,9 +28,9 @@ import { SignInWithSRPOutput, } from '../types'; import { - cleanActiveSignInState, + resetActiveSignInState, setActiveSignInState, -} from '../../../client/utils/store'; +} from '../../../client/utils/store/signInStore'; import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; @@ -102,7 +102,7 @@ export async function signInWithSRP( }), signInDetails, }); - cleanActiveSignInState(); + resetActiveSignInState(); await dispatchSignedInHubEvent(); @@ -119,7 +119,7 @@ export async function signInWithSRP( challengeParameters: handledChallengeParameters as ChallengeParameters, }); } catch (error) { - cleanActiveSignInState(); + resetActiveSignInState(); resetAutoSignIn(); assertServiceError(error); const result = getSignInResultFromError(error.name); diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts index 94165046864..4f653f46c94 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserAuth.ts @@ -26,11 +26,11 @@ import { SignInWithUserAuthInput, SignInWithUserAuthOutput, } from '../types'; +import { autoSignInStore } from '../../../client/utils/store'; import { - autoSignInStore, - cleanActiveSignInState, + resetActiveSignInState, setActiveSignInState, -} from '../../../client/utils/store'; +} from '../../../client/utils/store/signInStore'; import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; import { tokenOrchestrator } from '../tokenProvider'; @@ -111,7 +111,7 @@ export async function signInWithUserAuth( }), signInDetails, }); - cleanActiveSignInState(); + resetActiveSignInState(); await dispatchSignedInHubEvent(); @@ -132,7 +132,7 @@ export async function signInWithUserAuth( : undefined, }); } catch (error) { - cleanActiveSignInState(); + resetActiveSignInState(); resetAutoSignIn(); assertServiceError(error); const result = getSignInResultFromError(error.name); diff --git a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts index 8027af90c71..56e1c1af9f5 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithUserPassword.ts @@ -26,9 +26,9 @@ import { SignInWithUserPasswordOutput, } from '../types'; import { - cleanActiveSignInState, + resetActiveSignInState, setActiveSignInState, -} from '../../../client/utils/store'; +} from '../../../client/utils/store/signInStore'; import { cacheCognitoTokens } from '../tokenProvider/cacheTokens'; import { tokenOrchestrator } from '../tokenProvider'; import { dispatchSignedInHubEvent } from '../utils/dispatchSignedInHubEvent'; @@ -97,7 +97,7 @@ export async function signInWithUserPassword( }), signInDetails, }); - cleanActiveSignInState(); + resetActiveSignInState(); await dispatchSignedInHubEvent(); @@ -114,7 +114,7 @@ export async function signInWithUserPassword( challengeParameters: retriedChallengeParameters as ChallengeParameters, }); } catch (error) { - cleanActiveSignInState(); + resetActiveSignInState(); resetAutoSignIn(); assertServiceError(error); const result = getSignInResultFromError(error.name); diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index cadaa7d905c..9d90fade9d2 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -383,7 +383,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "28.46 kB" + "limit": "28.60 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -449,7 +449,7 @@ "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "30.56 kB" + "limit": "30.87 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", @@ -515,7 +515,7 @@ "name": "[Storage] uploadData (S3)", "path": "./dist/esm/storage/index.mjs", "import": "{ uploadData }", - "limit": "22.81 kB" + "limit": "22.84 kB" } ] } diff --git a/packages/core/__tests__/storage/SyncSessionStorage.test.ts b/packages/core/__tests__/storage/SyncSessionStorage.test.ts new file mode 100644 index 00000000000..fb56565d598 --- /dev/null +++ b/packages/core/__tests__/storage/SyncSessionStorage.test.ts @@ -0,0 +1,69 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { SyncSessionStorage } from '../../src/storage/SyncSessionStorage'; + +describe('SyncSessionStorage', () => { + let sessionStorage: SyncSessionStorage; + const signInStateKeys: Record = { + username: 'CognitoSignInState.username', + challengeName: 'CognitoSignInState.challengeName', + signInSession: 'CognitoSignInState.signInSession', + expiry: 'CognitoSignInState.expiry', + }; + + const user1 = { + username: 'joonchoi', + challengeName: 'CUSTOM_CHALLENGE', + signInSession: '888577-ltfgo-42d8-891d-666l858766g7', + expiry: '1234567', + }; + + beforeEach(() => { + sessionStorage = new SyncSessionStorage(); + }); + + it('can set and retrieve item by key', () => { + sessionStorage.setItem(signInStateKeys.username, user1.username); + + expect(sessionStorage.getItem(signInStateKeys.username)).toBe( + user1.username, + ); + }); + + it('can override item by setting with the same key', () => { + const newUserName = 'joonchoi+test'; + sessionStorage.setItem(signInStateKeys.username, user1.username); + expect(sessionStorage.getItem(signInStateKeys.username)).toBe( + user1.username, + ); + sessionStorage.setItem(signInStateKeys.username, newUserName); + + expect(sessionStorage.getItem(signInStateKeys.username)).toBe(newUserName); + }); + + it('can remove item by key', () => { + const newUserName = 'joonchoi+tobedeleted'; + sessionStorage.setItem(signInStateKeys.username, newUserName); + expect(sessionStorage.getItem(signInStateKeys.username)).toBe(newUserName); + sessionStorage.removeItem(signInStateKeys.username); + expect(sessionStorage.getItem(signInStateKeys.username)).toBeNull(); + }); + + it('clears all items', () => { + sessionStorage.setItem(signInStateKeys.username, user1.username); + sessionStorage.setItem(signInStateKeys.signInSession, user1.signInSession); + + sessionStorage.clear(); + + expect(sessionStorage.getItem(signInStateKeys.username)).toBeNull(); + expect(sessionStorage.getItem(signInStateKeys.signInSession)).toBeNull(); + }); + + it('will not throw if trying to delete a non existing key', () => { + const badKey = 'nonExistingKey'; + + expect(() => { + sessionStorage.removeItem(badKey); + }).not.toThrow(); + }); +}); diff --git a/packages/core/__tests__/storage/storage-mechanisms-node-runtime.test.ts b/packages/core/__tests__/storage/storage-mechanisms-node-runtime.test.ts deleted file mode 100644 index 0566237962b..00000000000 --- a/packages/core/__tests__/storage/storage-mechanisms-node-runtime.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @jest-environment node - */ - -import { AmplifyError, AmplifyErrorCode } from '../../src/libraryUtils'; -import { defaultStorage, sessionStorage } from '../../src/storage'; - -const key = 'k'; -const value = 'value'; - -describe('test mechanisms', () => { - test('test defaultStorage operations in node environment', async () => { - try { - await defaultStorage.setItem(key, value); - } catch (error: any) { - expect(error).toBeInstanceOf(AmplifyError); - expect(error.name).toBe(AmplifyErrorCode.PlatformNotSupported); - } - }); - - test('test sessionStorage operations in node environment', async () => { - try { - await sessionStorage.setItem(key, value); - } catch (error: any) { - expect(error).toBeInstanceOf(AmplifyError); - expect(error.name).toBe(AmplifyErrorCode.PlatformNotSupported); - } - }); -}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f17968928c6..2ead3026682 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -59,6 +59,7 @@ export { CookieStorage, defaultStorage, sessionStorage, + syncSessionStorage, sharedInMemoryStorage, } from './storage'; export { KeyValueStorageInterface } from './types'; diff --git a/packages/core/src/storage/SyncKeyValueStorage.ts b/packages/core/src/storage/SyncKeyValueStorage.ts new file mode 100644 index 00000000000..33885f2af31 --- /dev/null +++ b/packages/core/src/storage/SyncKeyValueStorage.ts @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { PlatformNotSupportedError } from '../errors'; +import { SyncStorage } from '../types'; + +/** + * @internal + */ +export class SyncKeyValueStorage implements SyncStorage { + _storage?: Storage; + + constructor(storage?: Storage) { + this._storage = storage; + } + + get storage() { + if (!this._storage) throw new PlatformNotSupportedError(); + + return this._storage; + } + + /** + * This is used to set a specific item in storage + * @param {string} key - the key for the item + * @param {object} value - the value + * @returns {string} value that was set + */ + setItem(key: string, value: string) { + this.storage.setItem(key, value); + } + + /** + * This is used to get a specific key from storage + * @param {string} key - the key for the item + * This is used to clear the storage + * @returns {string} the data item + */ + getItem(key: string) { + return this.storage.getItem(key); + } + + /** + * This is used to remove an item from storage + * @param {string} key - the key being set + * @returns {string} value - value that was deleted + */ + removeItem(key: string) { + this.storage.removeItem(key); + } + + /** + * This is used to clear the storage + * @returns {string} nothing + */ + clear() { + this.storage.clear(); + } +} diff --git a/packages/core/src/storage/SyncSessionStorage.ts b/packages/core/src/storage/SyncSessionStorage.ts new file mode 100644 index 00000000000..83224bfc878 --- /dev/null +++ b/packages/core/src/storage/SyncSessionStorage.ts @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SyncKeyValueStorage } from './SyncKeyValueStorage'; +import { getSessionStorageWithFallback } from './utils'; + +/** + * @internal + */ +export class SyncSessionStorage extends SyncKeyValueStorage { + constructor() { + super(getSessionStorageWithFallback()); + } +} diff --git a/packages/core/src/storage/index.ts b/packages/core/src/storage/index.ts index 29fba0b3dea..72219167521 100644 --- a/packages/core/src/storage/index.ts +++ b/packages/core/src/storage/index.ts @@ -5,9 +5,11 @@ import { DefaultStorage } from './DefaultStorage'; import { InMemoryStorage } from './InMemoryStorage'; import { KeyValueStorage } from './KeyValueStorage'; import { SessionStorage } from './SessionStorage'; +import { SyncSessionStorage } from './SyncSessionStorage'; export { CookieStorage } from './CookieStorage'; export const defaultStorage = new DefaultStorage(); export const sessionStorage = new SessionStorage(); +export const syncSessionStorage = new SyncSessionStorage(); export const sharedInMemoryStorage = new KeyValueStorage(new InMemoryStorage()); diff --git a/packages/core/src/types/storage.ts b/packages/core/src/types/storage.ts index d664a95b446..79a0c63e74b 100644 --- a/packages/core/src/types/storage.ts +++ b/packages/core/src/types/storage.ts @@ -21,3 +21,10 @@ export interface CookieStorageData { secure?: boolean; sameSite?: SameSite; } + +export interface SyncStorage { + setItem(key: string, value: string): void; + getItem(key: string): string | null; + removeItem(key: string): void; + clear(): void; +}