Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(auth): add authenticated client side validation #12033

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
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';
import { RespondToAuthChallengeCommandOutput } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/types';
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 = {
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 21 additions & 4 deletions packages/auth/__tests__/providers/cognito/signInWithSRP.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { AmplifyErrorString } from '@aws-amplify/core/internals/utils';

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';
import { RespondToAuthChallengeCommandOutput } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/types';
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 = {
Expand Down Expand Up @@ -103,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
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';
import { RespondToAuthChallengeCommandOutput } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/types';
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 = {
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions packages/auth/src/errors/constants.ts
Original file line number Diff line number Diff line change
@@ -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';
23 changes: 21 additions & 2 deletions packages/auth/src/providers/cognito/apis/signIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import { signInWithCustomAuth } from './signInWithCustomAuth';
import { signInWithCustomSRPAuth } from './signInWithCustomSRPAuth';
import { signInWithSRP } from './signInWithSRP';
import { signInWithUserPassword } from './signInWithUserPassword';
import { AuthSignInResult, SignInRequest } from '../../../types';
import { AuthSignInResult, AuthUser, SignInRequest } from '../../../types';
import { CognitoSignInOptions } from '../types';
import { getCurrentUser } from './getCurrentUser';
import { AuthError } from '../../../errors/AuthError';
import { USER_ALREADY_AUTHENTICATED_EXCEPTION } from '../../../errors/constants';

/**
* Signs a user in
*
Expand All @@ -27,7 +31,7 @@ export async function signIn(
signInRequest: SignInRequest<CognitoSignInOptions>
): Promise<AuthSignInResult> {
const authFlowType = signInRequest.options?.serviceOptions?.authFlowType;

await isUserAuthenticated();
switch (authFlowType) {
case 'USER_SRP_AUTH':
return signInWithSRP(signInRequest);
Expand All @@ -41,3 +45,18 @@ export async function signIn(
return signInWithSRP(signInRequest);
}
}

async function isUserAuthenticated() {
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 signed in user because auth tokens were found.',
recoverySuggestion: 'Please call signOut before calling signIn again.',
});
}
}
19 changes: 13 additions & 6 deletions packages/auth/src/providers/cognito/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,8 +31,10 @@ 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:
'Please try to sign in first before calling this API again',
israx marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
Expand All @@ -41,8 +44,10 @@ 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:
'Please try to sign in first before calling this API again',
israx marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
Expand All @@ -52,8 +57,10 @@ 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:
'Please try to sign in first before calling this API again',
israx marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
Expand Down
Loading