diff --git a/packages/auth/__tests__/providers/cognito/utils/getAuthSessionValidity.test.ts b/packages/auth/__tests__/providers/cognito/utils/getAuthSessionValidity.test.ts new file mode 100644 index 00000000000..18ff88fbb0c --- /dev/null +++ b/packages/auth/__tests__/providers/cognito/utils/getAuthSessionValidity.test.ts @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + DEFAULT_AUTH_SESSION_VALIDITY_MS, + getAuthSessionValidity, +} from '../../../../src/providers/cognito/utils/getAuthSessionValidity'; + +jest.mock( + '@aws-amplify/core', + () => ({ + Amplify: { + getConfig: jest.fn(), + }, + }), + { virtual: true }, +); + +describe('getAuthSessionValidity()', () => { + const mockGetConfig = ( + jest.requireMock('@aws-amplify/core') as { + Amplify: { getConfig: jest.Mock }; + } + ).Amplify.getConfig; + + afterEach(() => { + mockGetConfig.mockReset(); + }); + + it('returns the default Cognito auth session validity when not configured', () => { + mockGetConfig.mockReturnValue({} as any); + + expect(getAuthSessionValidity()).toBe(DEFAULT_AUTH_SESSION_VALIDITY_MS); + }); + + it('returns configured Cognito auth session validity in milliseconds', () => { + mockGetConfig.mockReturnValue({ + Auth: { + Cognito: { + userPoolId: 'us-west-2_test', + userPoolClientId: 'clientId', + authSessionValidity: 15, + }, + }, + }); + + expect(getAuthSessionValidity()).toBe(15 * 60 * 1000); + }); + + it('bounds configured Cognito auth session validity to the Cognito range', () => { + mockGetConfig.mockReturnValueOnce({ + Auth: { + Cognito: { + userPoolId: 'us-west-2_test', + userPoolClientId: 'clientId', + authSessionValidity: 2, + }, + }, + }); + expect(getAuthSessionValidity()).toBe(3 * 60 * 1000); + + mockGetConfig.mockReturnValueOnce({ + Auth: { + Cognito: { + userPoolId: 'us-west-2_test', + userPoolClientId: 'clientId', + authSessionValidity: 16, + }, + }, + }); + expect(getAuthSessionValidity()).toBe(15 * 60 * 1000); + }); +}); diff --git a/packages/auth/src/client/utils/store/signInStore.ts b/packages/auth/src/client/utils/store/signInStore.ts index ceb86815693..0327bfe11da 100644 --- a/packages/auth/src/client/utils/store/signInStore.ts +++ b/packages/auth/src/client/utils/store/signInStore.ts @@ -5,6 +5,7 @@ import { syncSessionStorage } from '@aws-amplify/core'; import { CognitoAuthSignInDetails } from '../../../providers/cognito/types'; import { ChallengeName } from '../../../foundation/factories/serviceClients/cognitoIdentityProvider/types'; +import { getAuthSessionValidity } from '../../../providers/cognito/utils/getAuthSessionValidity'; import { Reducer, Store } from './types'; @@ -24,9 +25,6 @@ type SignInAction = | { type: 'SET_SIGN_IN_SESSION'; value?: string } | { type: 'RESET_STATE' }; -// Minutes until stored session invalidates is defaulted to 3 minutes -// to maintain parity with Amazon Cognito user pools API behavior -const MS_TO_EXPIRY = 3 * 60 * 1000; const TGT_STATE = 'CognitoSignInState'; const SIGN_IN_STATE_KEYS = { username: `${TGT_STATE}.username`, @@ -166,7 +164,7 @@ export const persistSignInState = ({ // Updates expiry when session is passed syncSessionStorage.setItem( SIGN_IN_STATE_KEYS.expiry, - String(Date.now() + MS_TO_EXPIRY), + String(Date.now() + getAuthSessionValidity()), ); } }; diff --git a/packages/auth/src/providers/cognito/utils/getAuthSessionValidity.ts b/packages/auth/src/providers/cognito/utils/getAuthSessionValidity.ts new file mode 100644 index 00000000000..373dfd26925 --- /dev/null +++ b/packages/auth/src/providers/cognito/utils/getAuthSessionValidity.ts @@ -0,0 +1,28 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Amplify } from '@aws-amplify/core'; + +export const DEFAULT_AUTH_SESSION_VALIDITY_MS = 3 * 60 * 1000; +const MIN_AUTH_SESSION_VALIDITY_MS = 3 * 60 * 1000; +const MAX_AUTH_SESSION_VALIDITY_MS = 15 * 60 * 1000; + +export const getAuthSessionValidity = (): number => { + const authSessionValidity = + Amplify.getConfig().Auth?.Cognito.authSessionValidity; + + if ( + typeof authSessionValidity !== 'number' || + !Number.isFinite(authSessionValidity) + ) { + return DEFAULT_AUTH_SESSION_VALIDITY_MS; + } + + const authSessionValidityMs = authSessionValidity * 60 * 1000; + const boundedAuthSessionValidity = Math.min( + Math.max(authSessionValidityMs, MIN_AUTH_SESSION_VALIDITY_MS), + MAX_AUTH_SESSION_VALIDITY_MS, + ); + + return boundedAuthSessionValidity; +}; diff --git a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts index 9bebcf4be82..3abbb49b515 100644 --- a/packages/auth/src/providers/cognito/utils/signUpHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signUpHelpers.ts @@ -12,7 +12,7 @@ import { resetAutoSignIn, setAutoSignIn } from '../apis/autoSignIn'; import { AUTO_SIGN_IN_EXCEPTION } from '../../../errors/constants'; import { signInWithUserAuth } from '../apis/signInWithUserAuth'; -const MAX_AUTOSIGNIN_POLLING_MS = 3 * 60 * 1000; +import { getAuthSessionValidity } from './getAuthSessionValidity'; export function handleCodeAutoSignIn(signInInput: SignInInput) { const stopHubListener = HubInternal.listen( @@ -38,7 +38,7 @@ export function handleCodeAutoSignIn(signInInput: SignInInput) { stopHubListener(); clearTimeout(timeOutId); resetAutoSignIn(); - }, MAX_AUTOSIGNIN_POLLING_MS); + }, getAuthSessionValidity()); } // Debounces the auto sign-in flow with link @@ -70,7 +70,7 @@ function handleAutoSignInWithLink( const start = Date.now(); const autoSignInPollingIntervalId = setInterval(async () => { const elapsedTime = Date.now() - start; - const maxTime = MAX_AUTOSIGNIN_POLLING_MS; + const maxTime = getAuthSessionValidity(); if (elapsedTime > maxTime) { clearInterval(autoSignInPollingIntervalId); reject( diff --git a/packages/core/__tests__/parseAWSExports.test.ts b/packages/core/__tests__/parseAWSExports.test.ts index d021792bd2f..6372d26d87b 100644 --- a/packages/core/__tests__/parseAWSExports.test.ts +++ b/packages/core/__tests__/parseAWSExports.test.ts @@ -87,6 +87,7 @@ describe('parseAWSExports', () => { Cognito: { identityPoolId, allowGuestAccess: false, + authSessionValidity: 15, loginWith: { email: false, oauth: { @@ -157,6 +158,7 @@ describe('parseAWSExports', () => { parseAWSExports({ aws_project_region: 'us-west-2', aws_cognito_identity_pool_id: identityPoolId, + aws_cognito_auth_session_validity: 15, aws_cognito_sign_up_verification_method: signUpVerificationMethod, aws_cognito_username_attributes: ['PHONE_NUMBER'], aws_cognito_signup_attributes: ['PHONE_NUMBER'], diff --git a/packages/core/__tests__/parseAmplifyOutputs.test.ts b/packages/core/__tests__/parseAmplifyOutputs.test.ts index 3e0636e0a47..3561522dc48 100644 --- a/packages/core/__tests__/parseAmplifyOutputs.test.ts +++ b/packages/core/__tests__/parseAmplifyOutputs.test.ts @@ -110,6 +110,7 @@ describe('parseAmplifyOutputs tests', () => { auth: { user_pool_id: 'us-east-1:', user_pool_client_id: 'xxxx', + auth_session_validity: 15, aws_region: 'us-east-1', identity_pool_id: 'test', oauth: { @@ -162,6 +163,7 @@ describe('parseAmplifyOutputs tests', () => { }, userPoolClientId: 'xxxx', userPoolId: 'us-east-1:', + authSessionValidity: 15, loginWith: { email: true, phone: false, diff --git a/packages/core/src/parseAWSExports.ts b/packages/core/src/parseAWSExports.ts index 295c9e211e8..b68b710b948 100644 --- a/packages/core/src/parseAWSExports.ts +++ b/packages/core/src/parseAWSExports.ts @@ -55,6 +55,7 @@ export const parseAWSExports = ( aws_appsync_region, aws_bots_config, aws_cognito_identity_pool_id, + aws_cognito_auth_session_validity, aws_cognito_sign_up_verification_method, aws_cognito_mfa_configuration, aws_cognito_mfa_types, @@ -178,6 +179,7 @@ export const parseAWSExports = ( ) ?? false, } : undefined; + const authSessionValidityConfig = aws_cognito_auth_session_validity; const mergedUserAttributes: LegacyUserAttributeKey[] = Array.from( new Set([ ...(aws_cognito_verification_mechanisms ?? []), @@ -216,6 +218,11 @@ export const parseAWSExports = ( }, }, }; + + if (authSessionValidityConfig !== undefined) { + amplifyConfig.Auth.Cognito.authSessionValidity = + authSessionValidityConfig; + } } const hasOAuthConfig = oauth ? Object.keys(oauth).length > 0 : false; diff --git a/packages/core/src/parseAmplifyOutputs.ts b/packages/core/src/parseAmplifyOutputs.ts index 342db054891..e34c78194ae 100644 --- a/packages/core/src/parseAmplifyOutputs.ts +++ b/packages/core/src/parseAmplifyOutputs.ts @@ -81,6 +81,7 @@ function parseAuth( const { user_pool_id, user_pool_client_id, + auth_session_validity, identity_pool_id, password_policy, mfa_configuration, @@ -101,6 +102,10 @@ function parseAuth( }, } as AuthConfig; + if (auth_session_validity !== undefined) { + authConfig.Cognito.authSessionValidity = auth_session_validity; + } + if (identity_pool_id) { authConfig.Cognito = { ...authConfig.Cognito, diff --git a/packages/core/src/singleton/AmplifyOutputs/types.ts b/packages/core/src/singleton/AmplifyOutputs/types.ts index 20738f7cf50..6170cc42afc 100644 --- a/packages/core/src/singleton/AmplifyOutputs/types.ts +++ b/packages/core/src/singleton/AmplifyOutputs/types.ts @@ -20,6 +20,7 @@ export interface AmplifyOutputsAuthProperties { authentication_flow_type?: string; user_pool_id: string; user_pool_client_id: string; + auth_session_validity?: number; identity_pool_id?: string; password_policy?: { min_length: number; diff --git a/packages/core/src/singleton/Auth/types.ts b/packages/core/src/singleton/Auth/types.ts index ac0a88310cc..5dbcffc7b99 100644 --- a/packages/core/src/singleton/Auth/types.ts +++ b/packages/core/src/singleton/Auth/types.ts @@ -137,6 +137,7 @@ export interface AuthIdentityPoolConfig { userPoolClientId?: never; userPoolId?: never; userPoolEndpoint?: never; + authSessionValidity?: never; loginWith?: never; signUpVerificationMethod?: never; userAttributes?: never; @@ -179,6 +180,12 @@ export interface CognitoUserPoolConfig { * Use this field to specify a custom endpoint for the Amazon Cognito user pool. Ensure this endpoint is correct and valid. */ userPoolEndpoint?: string; + /** + * Auth session duration in minutes for Cognito API challenge flows. + * This should match the user pool app client's AuthSessionValidity setting. + * Valid values are from 3 to 15 minutes. + */ + authSessionValidity?: number; signUpVerificationMethod?: 'code' | 'link'; loginWith?: { oauth?: OAuthConfig;