diff --git a/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts b/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts index fe5e095e77b..3e042006724 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithCustomAuth.test.ts @@ -5,15 +5,18 @@ import { Amplify } from 'aws-amplify'; import { AuthError } from '../../../src/errors/AuthError'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { authAPITestParams } from './testUtils/authApiTestParams'; -import { signIn } from '../../../src/providers/cognito/apis/signIn'; +import { signIn, getCurrentUser } from '../../../src/providers/cognito'; import { signInWithCustomAuth } from '../../../src/providers/cognito/apis/signInWithCustomAuth'; import { InitiateAuthException } from '../../../src/providers/cognito/types/errors'; import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; import { InitiateAuthCommandOutput } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/types'; import { fetchTransferHandler } from '@aws-amplify/core/internals/aws-client-utils'; import { buildMockErrorResponse, mockJsonResponse } from './testUtils/data'; +import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../src/errors/constants'; +jest.mock('../../../src/providers/cognito/apis/getCurrentUser'); jest.mock('@aws-amplify/core/lib/clients/handlers/fetch'); + const authConfig = { Cognito: { userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', @@ -83,6 +86,24 @@ describe('signIn API happy path cases', () => { }); describe('signIn API error path cases:', () => { + test('signIn API should throw a validation AuthError when a user is already signed-in', async () => { + const mockedGetCurrentUser = getCurrentUser as jest.Mock; + + mockedGetCurrentUser.mockImplementationOnce(async () => { + return { + username: 'username', + userId: 'userId', + }; + }); + + try { + await signIn({ username: 'username', password: 'password' }); + } catch (error) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe(USER_ALREADY_AUTHENTICATED_EXCEPTION); + } + mockedGetCurrentUser.mockClear(); + }); test('signIn API should throw a validation AuthError when username is empty', async () => { expect.assertions(2); try { diff --git a/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts b/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts index ad639174b76..9c4189d11d8 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithCustomSRPAuth.test.ts @@ -4,7 +4,7 @@ import { AuthError } from '../../../src/errors/AuthError'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { authAPITestParams } from './testUtils/authApiTestParams'; -import { signIn } from '../../../src/providers/cognito/apis/signIn'; +import { signIn, getCurrentUser } from '../../../src/providers/cognito'; import { InitiateAuthException } from '../../../src/providers/cognito/types/errors'; import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; import { signInWithCustomSRPAuth } from '../../../src/providers/cognito/apis/signInWithCustomSRPAuth'; @@ -12,6 +12,8 @@ import { RespondToAuthChallengeCommandOutput } from '../../../src/providers/cogn import { Amplify } from 'aws-amplify'; import { fetchTransferHandler } from '@aws-amplify/core/internals/aws-client-utils'; import { buildMockErrorResponse, mockJsonResponse } from './testUtils/data'; +import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../src/errors/constants'; +jest.mock('../../../src/providers/cognito/apis/getCurrentUser'); jest.mock('@aws-amplify/core/lib/clients/handlers/fetch'); const authConfig = { @@ -89,6 +91,24 @@ describe('signIn API happy path cases', () => { }); describe('signIn API error path cases:', () => { + test('signIn API should throw a validation AuthError when a user is already signed-in', async () => { + const mockedGetCurrentUser = getCurrentUser as jest.Mock; + + mockedGetCurrentUser.mockImplementationOnce(async () => { + return { + username: 'username', + userId: 'userId', + }; + }); + + try { + await signIn({ username: 'username', password: 'password' }); + } catch (error) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe(USER_ALREADY_AUTHENTICATED_EXCEPTION); + } + mockedGetCurrentUser.mockClear(); + }); test('signIn API should throw a validation AuthError when username is empty', async () => { expect.assertions(2); try { diff --git a/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts b/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts index be8a84f3228..55f2968d754 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts @@ -1,9 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + import { AuthError } from '../../../src/errors/AuthError'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { authAPITestParams } from './testUtils/authApiTestParams'; -import { signIn } from '../../../src/providers/cognito/apis/signIn'; +import { signIn, getCurrentUser } from '../../../src/providers/cognito'; import { signInWithSRP } from '../../../src/providers/cognito/apis/signInWithSRP'; import { InitiateAuthException } from '../../../src/providers/cognito/types/errors'; import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; @@ -12,6 +13,8 @@ import { Amplify } from 'aws-amplify'; import { fetchTransferHandler } from '@aws-amplify/core/internals/aws-client-utils'; import { buildMockErrorResponse, mockJsonResponse } from './testUtils/data'; import { CognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider'; +import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../src/errors/constants'; +jest.mock('../../../src/providers/cognito/apis/getCurrentUser'); jest.mock('@aws-amplify/core/lib/clients/handlers/fetch'); const authConfig = { @@ -99,6 +102,24 @@ describe('signIn API happy path cases', () => { }); describe('signIn API error path cases:', () => { + test('signIn API should throw a validation AuthError when a user is already signed-in', async () => { + const mockedGetCurrentUser = getCurrentUser as jest.Mock; + + mockedGetCurrentUser.mockImplementationOnce(async () => { + return { + username: 'username', + userId: 'userId', + }; + }); + + try { + await signIn({ username: 'username', password: 'password' }); + } catch (error) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe(USER_ALREADY_AUTHENTICATED_EXCEPTION); + } + mockedGetCurrentUser.mockClear(); + }); test('signIn API should throw a validation AuthError when username is empty', async () => { expect.assertions(2); try { diff --git a/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts b/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts index 93e3fc71c18..e21b8d7d990 100644 --- a/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts +++ b/packages/auth/__tests__/providers/cognito/signInWithUserPassword.test.ts @@ -4,7 +4,7 @@ import { AuthError } from '../../../src/errors/AuthError'; import { AuthValidationErrorCode } from '../../../src/errors/types/validation'; import { authAPITestParams } from './testUtils/authApiTestParams'; -import { signIn } from '../../../src/providers/cognito/apis/signIn'; +import { signIn, getCurrentUser } from '../../../src/providers/cognito'; import { InitiateAuthException } from '../../../src/providers/cognito/types/errors'; import * as initiateAuthHelpers from '../../../src/providers/cognito/utils/signInHelpers'; import { signInWithUserPassword } from '../../../src/providers/cognito/apis/signInWithUserPassword'; @@ -12,8 +12,9 @@ import { RespondToAuthChallengeCommandOutput } from '../../../src/providers/cogn import { Amplify } from 'aws-amplify'; import { fetchTransferHandler } from '@aws-amplify/core/internals/aws-client-utils'; import { buildMockErrorResponse, mockJsonResponse } from './testUtils/data'; -import { cognitoCredentialsProvider } from '../../../src/providers/cognito/credentialsProvider'; import { CognitoUserPoolsTokenProvider } from '../../../src/providers/cognito/tokenProvider'; +import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../src/errors/constants'; +jest.mock('../../../src/providers/cognito/apis/getCurrentUser'); jest.mock('@aws-amplify/core/lib/clients/handlers/fetch'); const authConfig = { @@ -22,12 +23,7 @@ const authConfig = { userPoolId: 'us-west-2_zzzzz', }, }; -const authConfigWithClientmetadata = { - Cognito: { - userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - userPoolId: 'us-west-2_zzzzz', - }, -}; + CognitoUserPoolsTokenProvider.setAuthConfig(authConfig); Amplify.configure({ Auth: authConfig, @@ -82,6 +78,24 @@ describe('signIn API happy path cases', () => { }); describe('signIn API error path cases:', () => { + test('signIn API should throw a validation AuthError when a user is already signed-in', async () => { + const mockedGetCurrentUser = getCurrentUser as jest.Mock; + + mockedGetCurrentUser.mockImplementationOnce(async () => { + return { + username: 'username', + userId: 'userId', + }; + }); + + try { + await signIn({ username: 'username', password: 'password' }); + } catch (error) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe(USER_ALREADY_AUTHENTICATED_EXCEPTION); + } + mockedGetCurrentUser.mockClear(); + }); test('signIn API should throw a validation AuthError when username is empty', async () => { expect.assertions(2); try { diff --git a/packages/auth/src/errors/constants.ts b/packages/auth/src/errors/constants.ts new file mode 100644 index 00000000000..06baaade260 --- /dev/null +++ b/packages/auth/src/errors/constants.ts @@ -0,0 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const USER_UNAUTHENTICATED_EXCEPTION = 'UserUnAuthenticatedException'; +export const USER_ALREADY_AUTHENTICATED_EXCEPTION = + 'UserAlreadyAuthenticatedException'; diff --git a/packages/auth/src/providers/cognito/apis/signIn.ts b/packages/auth/src/providers/cognito/apis/signIn.ts index 46ec83c5eb1..e90348fbec6 100644 --- a/packages/auth/src/providers/cognito/apis/signIn.ts +++ b/packages/auth/src/providers/cognito/apis/signIn.ts @@ -9,6 +9,8 @@ import { signInWithCustomAuth } from './signInWithCustomAuth'; import { signInWithCustomSRPAuth } from './signInWithCustomSRPAuth'; import { signInWithSRP } from './signInWithSRP'; import { signInWithUserPassword } from './signInWithUserPassword'; +import { assertUserNotAuthenticated } from '../utils/signInHelpers'; + import { SignInInput, SignInOutput } from '../types'; /** * Signs a user in @@ -23,7 +25,7 @@ import { SignInInput, SignInOutput } from '../types'; */ export async function signIn(input: SignInInput): Promise { const authFlowType = input.options?.serviceOptions?.authFlowType; - + await assertUserNotAuthenticated(); switch (authFlowType) { case 'USER_SRP_AUTH': return signInWithSRP(input); diff --git a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts index d77d54bbfc5..839360360c8 100644 --- a/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts +++ b/packages/auth/src/providers/cognito/apis/signInWithRedirect.ts @@ -24,6 +24,7 @@ import { AuthError } from '../../../errors/AuthError'; import { AuthErrorTypes } from '../../../types/Auth'; import { AuthErrorCodes } from '../../../common/AuthErrorStrings'; import { authErrorMessages } from '../../../Errors'; +import { assertUserNotAuthenticated } from '../utils/signInHelpers'; import { SignInWithRedirectInput } from '../types'; const SELF = '_self'; @@ -35,7 +36,10 @@ const SELF = '_self'; * * TODO: add config errors */ -export function signInWithRedirect(input?: SignInWithRedirectInput): void { +export async function signInWithRedirect( + input?: SignInWithRedirectInput +): Promise { + await assertUserNotAuthenticated(); const authConfig = Amplify.getConfig().Auth?.Cognito; assertTokenProviderConfig(authConfig); assertOAuthConfig(authConfig); diff --git a/packages/auth/src/providers/cognito/utils/signInHelpers.ts b/packages/auth/src/providers/cognito/utils/signInHelpers.ts index 218fc3be082..d3324915aae 100644 --- a/packages/auth/src/providers/cognito/utils/signInHelpers.ts +++ b/packages/auth/src/providers/cognito/utils/signInHelpers.ts @@ -21,6 +21,7 @@ import { import { AuthError } from '../../../errors/AuthError'; import { InitiateAuthException } from '../types/errors'; import { + AuthUser, AuthUserAttribute, AuthMFAType, AuthTOTPSetupDetails, @@ -45,6 +46,8 @@ import { RespondToAuthChallengeCommandOutput, } from './clients/CognitoIdentityProvider/types'; import { getRegion } from './clients/CognitoIdentityProvider/utils'; +import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../errors/constants'; +import { getCurrentUser } from '../apis/getCurrentUser'; const USER_ATTRIBUTES = 'userAttributes.'; @@ -630,3 +633,19 @@ export function isMFATypeEnabled( if (!mfaTypes) return false; return mfaTypes.includes(mfaType); } + +export async function assertUserNotAuthenticated() { + let authUser: AuthUser | undefined; + try { + authUser = await getCurrentUser(); + } catch (error) {} + + if (authUser && authUser.userId && authUser.username) { + throw new AuthError({ + name: USER_ALREADY_AUTHENTICATED_EXCEPTION, + message: + 'There is already a signed in user.', + recoverySuggestion: 'Call signOut before calling signIn again.', + }); + } +} diff --git a/packages/auth/src/providers/cognito/utils/types.ts b/packages/auth/src/providers/cognito/utils/types.ts index 0bcccf0f0cc..9a364a0eec1 100644 --- a/packages/auth/src/providers/cognito/utils/types.ts +++ b/packages/auth/src/providers/cognito/utils/types.ts @@ -10,6 +10,7 @@ import { import { AuthError } from '../../../errors/AuthError'; import { CognitoAuthTokens } from '../tokenProvider/types'; +import { USER_UNAUTHENTICATED_EXCEPTION } from '../../../errors/constants'; export function isTypeUserPoolConfig( authConfig?: AuthConfig @@ -30,8 +31,9 @@ export function assertAuthTokens( ): asserts tokens is AuthTokens { if (!tokens || !tokens.accessToken) { throw new AuthError({ - name: 'Invalid Auth Tokens', - message: 'No Auth Tokens were found', + name: USER_UNAUTHENTICATED_EXCEPTION, + message: 'User needs to be authenticated to call this API.', + recoverySuggestion: 'Sign in before calling this API again.', }); } } @@ -41,8 +43,9 @@ export function assertIdTokenInAuthTokens( ): asserts tokens is AuthTokens { if (!tokens || !tokens.idToken) { throw new AuthError({ - name: 'IdToken not present in Auth Tokens', - message: 'No IdToken in Auth Tokens', + name: USER_UNAUTHENTICATED_EXCEPTION, + message: 'User needs to be authenticated to call this API.', + recoverySuggestion: 'Sign in before calling this API again.', }); } } @@ -52,8 +55,9 @@ export function assertAuthTokensWithRefreshToken( ): asserts tokens is CognitoAuthTokens & { refreshToken: string } { if (!tokens || !tokens.accessToken || !tokens.refreshToken) { throw new AuthError({ - name: 'Invalid Cognito Auth Tokens', - message: 'No Cognito Auth Tokens were found', + name: USER_UNAUTHENTICATED_EXCEPTION, + message: 'User needs to be authenticated to call this API.', + recoverySuggestion: 'Sign in before calling this API again.', }); } }