From 032ab8e4a0deeafa1c01692ea265508faecaf0e6 Mon Sep 17 00:00:00 2001 From: Chris F <5827964+cshfang@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:09:40 -0800 Subject: [PATCH] test: update signOut unit tests (#12561) * test: update signOut unit tests * Bump size limit (from adding logging) --- .../providers/cognito/signOut.test.ts | 517 +++++++----------- .../utils/oauth/completeOAuthSignOut.test.ts | 54 ++ .../oauth/handleOAuthSignOut.native.test.ts | 91 +++ .../utils/oauth/handleOAuthSignOut.test.ts | 58 ++ .../utils/oauth/oAuthSignOutRedirect.test.ts | 67 +++ .../src/providers/cognito/apis/signOut.ts | 17 +- packages/aws-amplify/package.json | 2 +- 7 files changed, 482 insertions(+), 324 deletions(-) create mode 100644 packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthSignOut.test.ts create mode 100644 packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts create mode 100644 packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts create mode 100644 packages/auth/__tests__/providers/cognito/utils/oauth/oAuthSignOutRedirect.test.ts diff --git a/packages/auth/__tests__/providers/cognito/signOut.test.ts b/packages/auth/__tests__/providers/cognito/signOut.test.ts index aa65a8d66c5..d1ff7c921b1 100644 --- a/packages/auth/__tests__/providers/cognito/signOut.test.ts +++ b/packages/auth/__tests__/providers/cognito/signOut.test.ts @@ -1,357 +1,238 @@ -import { Amplify } from '@aws-amplify/core'; -import { signOut } from '../../../src/providers/cognito'; -import * as TokenProvider from '../../../src/providers/cognito/tokenProvider'; -import { decodeJWT } from '@aws-amplify/core/internals/utils'; -import * as clients from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + Amplify, + clearCredentials, + ConsoleLogger, + Hub, +} from '@aws-amplify/core'; +import { AMPLIFY_SYMBOL } from '@aws-amplify/core/internals/utils'; +import { signOut } from '../../../src/providers/cognito/apis/signOut'; +import { tokenOrchestrator } from '../../../src/providers/cognito/tokenProvider'; +import { + globalSignOut, + revokeToken, +} from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider'; +import { getRegion } from '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/utils'; import { DefaultOAuthStore } from '../../../src/providers/cognito/utils/signInWithRedirectStore'; -import { openAuthSession } from '../../../src/utils'; - -jest.mock('@aws-amplify/core/dist/cjs/clients/handlers/fetch'); +import { handleOAuthSignOut } from '../../../src/providers/cognito/utils/oauth'; +import { AuthTokenStore } from '../../../src/providers/cognito/tokenProvider/types'; + +jest.mock('@aws-amplify/core', () => { + return { + ...(jest.genMockFromModule('@aws-amplify/core') as object), + // must do this as auth tests import `signInWithRedirect` + Amplify: { + getConfig: jest.fn().mockReturnValue({}), + }, + }; +}); +jest.mock('../../../src/providers/cognito/tokenProvider'); +jest.mock( + '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider' +); +jest.mock( + '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider/utils' +); +jest.mock('../../../src/providers/cognito/utils/oauth'); +jest.mock('../../../src/providers/cognito/utils/signInWithRedirectStore'); jest.mock('../../../src/utils'); -const mockedAccessToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJvcmlnaW5fanRpIjoiYXNjIn0.4X9nPnldRthcZwi9b0y3rvNn1jvzHnkgJjeEmzmq5VQ'; -const mockRefreshToken = 'abcdefghijk'; - -describe('signOut tests no oauth happy path', () => { - let tokenStoreSpy; - let tokenOrchestratorSpy; - let globalSignOutSpy; - let revokeTokenSpy; - - beforeEach(() => { - Amplify.configure( - { - Auth: { - Cognito: { - userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - userPoolId: 'us-west-2_zzzzz', - identityPoolId: 'us-west-2:xxxxxx', - }, - }, - }, - { - Auth: { - tokenProvider: TokenProvider.CognitoUserPoolsTokenProvider, - }, - } - ); - - revokeTokenSpy = jest - .spyOn(clients, 'revokeToken') - .mockImplementation(async () => { - return {}; - }); - - tokenStoreSpy = jest - .spyOn(TokenProvider.DefaultTokenStore.prototype, 'loadTokens') - .mockImplementation(async () => { - return { - accessToken: decodeJWT(mockedAccessToken), - refreshToken: mockRefreshToken, - clockDrift: 0, - }; - }); - - tokenOrchestratorSpy = jest - .spyOn(TokenProvider.tokenOrchestrator, 'clearTokens') - .mockImplementation(async () => {}); - - globalSignOutSpy = jest - .spyOn(clients, 'globalSignOut') - .mockImplementationOnce(async () => { - return { - $metadata: {}, - }; - }); - }); - test('test client signOut no oauth', async () => { - await signOut({ global: false }); - - expect(revokeTokenSpy).toBeCalledWith( - { - region: 'us-west-2', +describe('signOut', () => { + const accessToken = { payload: { origin_jti: 'revocation-id' } }; + const region = 'us-west-2'; + const cognitoConfig = { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: `${region}_zzzzz`, + identityPoolId: `${region}:xxxxxx`, + }; + const refreshToken = 'refresh-token'; + const cognitoAuthTokens = { + username: 'username', + clockDrift: 0, + accessToken, + refreshToken, + }; + // assert mocks + const mockAmplify = Amplify as jest.Mocked; + const mockClearCredentials = clearCredentials as jest.Mock; + const mockGetRegion = getRegion as jest.Mock; + const mockGlobalSignOut = globalSignOut as jest.Mock; + const mockHandleOAuthSignOut = handleOAuthSignOut as jest.Mock; + const mockHub = Hub as jest.Mocked; + const mockRevokeToken = revokeToken as jest.Mock; + const mockTokenOrchestrator = tokenOrchestrator as jest.Mocked< + typeof tokenOrchestrator + >; + const MockDefaultOAuthStore = DefaultOAuthStore as jest.Mock; + // create mocks + const mockLoadTokens = jest.fn(); + const mockAuthTokenStore = { + loadTokens: mockLoadTokens, + } as unknown as AuthTokenStore; + const mockDefaultOAuthStoreInstance = { + setAuthConfig: jest.fn(), + }; + // create spies + const loggerDebugSpy = jest.spyOn(ConsoleLogger.prototype, 'debug'); + // create test helpers + const expectSignOut = () => ({ + toComplete: () => { + expect(mockTokenOrchestrator.clearTokens).toBeCalledTimes(1); + expect(mockClearCredentials).toBeCalledTimes(1); + expect(mockHub.dispatch).toBeCalledWith( + 'auth', + { event: 'signedOut' }, + 'Auth', + AMPLIFY_SYMBOL + ); + }, + not: { + toComplete: () => { + expect(mockTokenOrchestrator.clearTokens).not.toBeCalled(); + expect(mockClearCredentials).not.toBeCalled(); + expect(mockHub.dispatch).not.toBeCalled(); }, - { - ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - Token: 'abcdefghijk', - } - ); - expect(globalSignOutSpy).not.toHaveBeenCalled(); - expect(tokenOrchestratorSpy).toBeCalled(); - expect(tokenStoreSpy).toBeCalled(); + }, }); - test('global sign out no oauth', async () => { - await signOut({ global: true }); - - expect(globalSignOutSpy).toBeCalledWith( - { - region: 'us-west-2', - }, - { - AccessToken: mockedAccessToken, - } + beforeAll(() => { + mockGetRegion.mockReturnValue(region); + MockDefaultOAuthStore.mockImplementation( + () => mockDefaultOAuthStoreInstance ); - - expect(tokenOrchestratorSpy).toBeCalled(); - expect(tokenStoreSpy).toBeCalled(); }); - beforeAll(() => { - jest.resetAllMocks(); + beforeEach(() => { + mockAmplify.getConfig.mockReturnValue({ Auth: { Cognito: cognitoConfig } }); + mockGlobalSignOut.mockResolvedValue({ $metadata: {} }); + mockRevokeToken.mockResolvedValue({}); + mockTokenOrchestrator.getTokenStore.mockReturnValue(mockAuthTokenStore); + mockLoadTokens.mockResolvedValue(cognitoAuthTokens); }); afterEach(() => { - jest.resetAllMocks(); + mockAmplify.getConfig.mockReset(); + mockGlobalSignOut.mockReset(); + mockRevokeToken.mockReset(); + mockClearCredentials.mockClear(); + mockGetRegion.mockClear(); + mockHub.dispatch.mockClear(); + mockTokenOrchestrator.clearTokens.mockClear(); + loggerDebugSpy.mockClear(); }); -}); -describe('signOut tests no oauth request fail', () => { - let tokenStoreSpy; - let tokenOrchestratorSpy; - let globalSignOutSpy; - let revokeTokenSpy; - let clearCredentialsSpy; - beforeAll(() => { - jest.resetAllMocks(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); + describe('Without OAuth configured', () => { + it('should perform client sign out on a revocable session', async () => { + await signOut(); + + expect(mockRevokeToken).toBeCalledWith( + { region }, + { ClientId: cognitoConfig.userPoolClientId, Token: refreshToken } + ); + expect(mockGetRegion).toBeCalledTimes(1); + expect(mockGlobalSignOut).not.toBeCalled(); + expectSignOut().toComplete(); + }); - beforeEach(() => { - Amplify.configure( - { - Auth: { - Cognito: { - userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - userPoolId: 'us-west-2_zzzzz', - identityPoolId: 'us-west-2:xxxxxx', - }, - }, - }, - { - Auth: { - tokenProvider: TokenProvider.CognitoUserPoolsTokenProvider, - credentialsProvider: { - clearCredentialsAndIdentityId() { - clearCredentialsSpy(); - }, - getCredentialsAndIdentityId(getCredentialsOptions) { - throw new Error('not implemented'); - }, - }, - }, - } - ); + it('should perform client sign out on an irrevocable session', async () => { + mockLoadTokens.mockResolvedValue({ + ...cognitoAuthTokens, + accessToken: {}, + }); - clearCredentialsSpy = jest.fn(() => {}); + await signOut(); - revokeTokenSpy = jest - .spyOn(clients, 'revokeToken') - .mockImplementation(async () => { - throw new Error('fail!!!'); - }); + expect(mockRevokeToken).not.toBeCalled(); + expect(mockGlobalSignOut).not.toBeCalled(); + expect(mockGetRegion).not.toBeCalled(); + expectSignOut().toComplete(); + }); - tokenStoreSpy = jest - .spyOn(TokenProvider.DefaultTokenStore.prototype, 'loadTokens') - .mockImplementation(async () => { - return { - accessToken: decodeJWT(mockedAccessToken), - refreshToken: mockRefreshToken, - clockDrift: 0, - }; - }); + it('should perform global sign out', async () => { + await signOut({ global: true }); - tokenOrchestratorSpy = jest - .spyOn(TokenProvider.tokenOrchestrator, 'clearTokens') - .mockImplementation(async () => {}); + expect(mockGlobalSignOut).toBeCalledWith( + { region: 'us-west-2' }, + { AccessToken: accessToken.toString() } + ); + expect(mockGetRegion).toBeCalledTimes(1); + expect(mockRevokeToken).not.toBeCalled(); + expectSignOut().toComplete(); + }); - globalSignOutSpy = jest - .spyOn(clients, 'globalSignOut') - .mockImplementation(async () => { - throw new Error('fail!!!'); - }); - }); + it('should still perform client sign out if token revoke fails', async () => { + mockRevokeToken.mockRejectedValue(new Error()); - test('test client signOut no oauth', async () => { - try { - await signOut({ global: false }); - } catch (err) { - fail('this shouldnt happen'); - } + await signOut(); - expect(revokeTokenSpy).toBeCalledWith( - { - region: 'us-west-2', - }, - { - ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - Token: 'abcdefghijk', - } - ); + expect(loggerDebugSpy).toBeCalledWith( + expect.stringContaining('Client signOut error caught') + ); + expect(mockGetRegion).toBeCalledTimes(1); + expectSignOut().toComplete(); + }); - expect(globalSignOutSpy).not.toHaveBeenCalled(); - expect(tokenOrchestratorSpy).toBeCalled(); - expect(tokenStoreSpy).toBeCalled(); - expect(clearCredentialsSpy).toBeCalled(); - }); + it('should still perform global sign out if token revoke fails', async () => { + mockGlobalSignOut.mockRejectedValue(new Error()); - test('global sign out no oauth', async () => { - try { await signOut({ global: true }); - } catch (err) { - fail('this shouldnt happen'); - } - expect(globalSignOutSpy).toBeCalledWith( - { - region: 'us-west-2', - }, - { - AccessToken: mockedAccessToken, - } - ); - - expect(tokenOrchestratorSpy).toBeCalled(); - expect(tokenStoreSpy).toBeCalled(); + expect(loggerDebugSpy).toBeCalledWith( + expect.stringContaining('Global signOut error caught') + ); + expect(mockGetRegion).toBeCalledTimes(1); + expectSignOut().toComplete(); + }); }); -}); -describe('signOut tests with oauth', () => { - let tokenStoreSpy; - let tokenOrchestratorSpy; - let globalSignOutSpy; - let revokeTokenSpy; - let clearCredentialsSpy; - let oauthStoreSpy; - const mockOpenAuthSession = openAuthSession as jest.Mock; - const originalWindowLocation = window.location; - beforeEach(() => { - Object.defineProperty(globalThis, 'window', { - value: { location: { origin: 'http://localhost:3000', pathname: '/' } }, - writable: true, - }); - Amplify.configure( - { - Auth: { - Cognito: { - userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - userPoolId: 'us-west-2_zzzzz', - identityPoolId: 'us-west-2:xxxxxx', - loginWith: { - oauth: { - domain: 'https://amazonaws.com', - redirectSignIn: ['http://localhost:3000/'], - redirectSignOut: ['http://localhost:3000/'], - responseType: 'code', - scopes: ['admin'], - }, - }, - }, + describe('With OAuth configured', () => { + const cognitoConfigWithOauth = { + ...cognitoConfig, + loginWith: { + oauth: { + domain: 'hosted-ui.test', + redirectSignIn: ['https://myapp.test/completeSignIn/'], + redirectSignOut: ['https://myapp.test/completeSignOut/'], + responseType: 'code' as 'code', // assert string union instead of string type + scopes: [], }, }, - { - Auth: { - tokenProvider: TokenProvider.CognitoUserPoolsTokenProvider, - credentialsProvider: { - clearCredentialsAndIdentityId() { - clearCredentialsSpy(); - }, - getCredentialsAndIdentityId(getCredentialsOptions) { - throw new Error('not implemented'); - }, - }, - }, - } - ); - oauthStoreSpy = jest - .spyOn(DefaultOAuthStore.prototype, 'loadOAuthSignIn') - .mockImplementation(async () => ({ - isOAuthSignIn: true, - preferPrivateSession: false, - })); + }; - revokeTokenSpy = jest - .spyOn(clients, 'revokeToken') - .mockImplementation(async () => { - return {}; + beforeEach(() => { + mockAmplify.getConfig.mockReturnValue({ + Auth: { Cognito: cognitoConfigWithOauth }, }); - - tokenStoreSpy = jest - .spyOn(TokenProvider.DefaultTokenStore.prototype, 'loadTokens') - .mockImplementation(async () => { - return { - accessToken: decodeJWT(mockedAccessToken), - refreshToken: mockRefreshToken, - clockDrift: 0, - }; - }); - - clearCredentialsSpy = jest.fn(() => {}); - - tokenOrchestratorSpy = jest - .spyOn(TokenProvider.tokenOrchestrator, 'clearTokens') - .mockImplementation(async () => {}); - - globalSignOutSpy = jest - .spyOn(clients, 'globalSignOut') - .mockImplementationOnce(async () => { - return { - $metadata: {}, - }; - }); - }); - afterEach(() => { - Object.defineProperty(globalThis, 'window', { - value: originalWindowLocation, + mockHandleOAuthSignOut.mockResolvedValue({ type: 'success' }); }); - }); - test('test client signOut with oauth', async () => { - await signOut({ global: false }); - - expect(revokeTokenSpy).toBeCalledWith( - { - region: 'us-west-2', - }, - { - ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - Token: 'abcdefghijk', - } - ); - expect(globalSignOutSpy).not.toHaveBeenCalled(); - expect(tokenOrchestratorSpy).toBeCalled(); - expect(tokenStoreSpy).toBeCalled(); - expect(mockOpenAuthSession).toBeCalledWith( - 'https://https://amazonaws.com/logout?client_id=111111-aaaaa-42d8-891d-ee81a1549398&logout_uri=http%3A%2F%2Flocalhost%3A3000%2F', - ['http://localhost:3000/'], - false - ); - expect(clearCredentialsSpy).toBeCalled(); - }); + afterEach(() => { + mockAmplify.getConfig.mockReset(); + mockHandleOAuthSignOut.mockReset(); + }); - test('global sign out with oauth', async () => { - await signOut({ global: true }); + it('should perform OAuth sign out', async () => { + await signOut(); + + expect(MockDefaultOAuthStore).toBeCalledTimes(1); + expect(mockDefaultOAuthStoreInstance.setAuthConfig).toBeCalledWith( + cognitoConfigWithOauth + ); + expect(mockHandleOAuthSignOut).toBeCalledWith( + cognitoConfigWithOauth, + mockDefaultOAuthStoreInstance + ); + // In cases of OAuth, token removal and Hub dispatch should be performed by the OAuth handling since + // these actions can be deferred or canceled out of altogether. + expectSignOut().not.toComplete(); + }); - expect(globalSignOutSpy).toBeCalledWith( - { - region: 'us-west-2', - }, - { - AccessToken: mockedAccessToken, - } - ); + it('should throw an error on OAuth failure', async () => { + mockHandleOAuthSignOut.mockResolvedValue({ type: 'error' }); - expect(tokenOrchestratorSpy).toBeCalled(); - expect(tokenStoreSpy).toBeCalled(); - expect(mockOpenAuthSession).toBeCalledWith( - 'https://https://amazonaws.com/logout?client_id=111111-aaaaa-42d8-891d-ee81a1549398&logout_uri=http%3A%2F%2Flocalhost%3A3000%2F', - ['http://localhost:3000/'], - false - ); - expect(clearCredentialsSpy).toBeCalled(); + await expect(signOut()).rejects.toThrow(); + }); }); }); diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthSignOut.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthSignOut.test.ts new file mode 100644 index 00000000000..3df60822a6d --- /dev/null +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthSignOut.test.ts @@ -0,0 +1,54 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { clearCredentials, Hub } from '@aws-amplify/core'; +import { AMPLIFY_SYMBOL } from '@aws-amplify/core/internals/utils'; +import { tokenOrchestrator } from '../../../../../src/providers/cognito/tokenProvider'; +import { completeOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut'; +import { DefaultOAuthStore } from '../../../../../src/providers/cognito/utils/signInWithRedirectStore'; + +jest.mock('@aws-amplify/core', () => { + return { + ...(jest.genMockFromModule('@aws-amplify/core') as object), + // must do this as auth tests import `signInWithRedirect` + Amplify: { + getConfig: jest.fn().mockReturnValue({}), + }, + }; +}); +jest.mock('../../../../../src/providers/cognito/tokenProvider'); + +describe('completeOAuthSignOut', () => { + // assert mocks + const mockClearCredentials = clearCredentials as jest.Mock; + const mockHub = Hub as jest.Mocked; + const mockTokenOrchestrator = tokenOrchestrator as jest.Mocked< + typeof tokenOrchestrator + >; + + // create mocks + const mockStore = { + clearOAuthData: jest.fn(), + } as unknown as jest.Mocked; + + afterEach(() => { + mockStore.clearOAuthData.mockClear(); + mockClearCredentials.mockClear(); + mockHub.dispatch.mockClear(); + mockTokenOrchestrator.clearTokens.mockClear(); + }); + + it('should complete OAuth sign out', async () => { + await completeOAuthSignOut(mockStore); + + expect(mockStore.clearOAuthData).toBeCalledTimes(1); + expect(mockTokenOrchestrator.clearTokens).toBeCalledTimes(1); + expect(mockClearCredentials).toBeCalledTimes(1); + expect(mockHub.dispatch).toBeCalledWith( + 'auth', + { event: 'signedOut' }, + 'Auth', + AMPLIFY_SYMBOL + ); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts new file mode 100644 index 00000000000..95a50618c5d --- /dev/null +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.native.test.ts @@ -0,0 +1,91 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { completeOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut'; +import { handleOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/handleOAuthSignOut.native'; +import { oAuthSignOutRedirect } from '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect'; +import { DefaultOAuthStore } from '../../../../../src/providers/cognito/utils/signInWithRedirectStore'; + +jest.mock( + '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut' +); +jest.mock( + '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect' +); + +describe('handleOAuthSignOut (native)', () => { + const region = 'us-west-2'; + const cognitoConfig = { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: `${region}_zzzzz`, + identityPoolId: `${region}:xxxxxx`, + }; + // assert mocks + const mockCompleteOAuthSignOut = completeOAuthSignOut as jest.Mock; + const mockOAuthSignOutRedirect = oAuthSignOutRedirect as jest.Mock; + // create mocks + const mockStore = { + loadOAuthSignIn: jest.fn(), + } as unknown as jest.Mocked; + + afterEach(() => { + mockStore.loadOAuthSignIn.mockReset(); + mockCompleteOAuthSignOut.mockClear(); + mockOAuthSignOutRedirect.mockClear(); + }); + + describe('when preferPrivateSession is false', () => { + beforeEach(() => { + mockStore.loadOAuthSignIn.mockResolvedValue({ + isOAuthSignIn: true, + preferPrivateSession: false, + }); + }); + it('should complete OAuth sign out and redirect', async () => { + mockOAuthSignOutRedirect.mockResolvedValue({ type: 'success' }); + await handleOAuthSignOut(cognitoConfig, mockStore); + + expect(mockOAuthSignOutRedirect).toBeCalledWith(cognitoConfig, false); + expect(mockCompleteOAuthSignOut).toBeCalledWith(mockStore); + }); + + it('should not complete OAuth sign out if redirect is canceled', async () => { + mockOAuthSignOutRedirect.mockResolvedValue({ type: 'canceled' }); + await handleOAuthSignOut(cognitoConfig, mockStore); + + expect(mockOAuthSignOutRedirect).toBeCalledWith(cognitoConfig, false); + expect(mockCompleteOAuthSignOut).not.toBeCalled(); + }); + + it('should not complete OAuth sign out if redirect failed', async () => { + mockOAuthSignOutRedirect.mockResolvedValue({ type: 'error' }); + await handleOAuthSignOut(cognitoConfig, mockStore); + + expect(mockOAuthSignOutRedirect).toBeCalledWith(cognitoConfig, false); + expect(mockCompleteOAuthSignOut).not.toBeCalled(); + }); + }); + + it('should always complete OAuth sign out and redirect when preferPrivateSession is true', async () => { + mockStore.loadOAuthSignIn.mockResolvedValue({ + isOAuthSignIn: true, + preferPrivateSession: true, + }); + mockOAuthSignOutRedirect.mockResolvedValue({ type: 'error' }); + await handleOAuthSignOut(cognitoConfig, mockStore); + + expect(mockOAuthSignOutRedirect).toBeCalledWith(cognitoConfig, true); + expect(mockCompleteOAuthSignOut).toBeCalledWith(mockStore); + }); + + it('should complete OAuth sign out but not redirect', async () => { + mockStore.loadOAuthSignIn.mockResolvedValue({ + isOAuthSignIn: false, + preferPrivateSession: false, + }); + await handleOAuthSignOut(cognitoConfig, mockStore); + + expect(mockOAuthSignOutRedirect).not.toBeCalled(); + expect(mockCompleteOAuthSignOut).toBeCalledWith(mockStore); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts new file mode 100644 index 00000000000..a938b16245b --- /dev/null +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { completeOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut'; +import { handleOAuthSignOut } from '../../../../../src/providers/cognito/utils/oauth/handleOAuthSignOut'; +import { oAuthSignOutRedirect } from '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect'; +import { DefaultOAuthStore } from '../../../../../src/providers/cognito/utils/signInWithRedirectStore'; + +jest.mock( + '../../../../../src/providers/cognito/utils/oauth/completeOAuthSignOut' +); +jest.mock( + '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect' +); + +describe('handleOAuthSignOut', () => { + const region = 'us-west-2'; + const cognitoConfig = { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: `${region}_zzzzz`, + identityPoolId: `${region}:xxxxxx`, + }; + // assert mocks + const mockCompleteOAuthSignOut = completeOAuthSignOut as jest.Mock; + const mockOAuthSignOutRedirect = oAuthSignOutRedirect as jest.Mock; + // create mocks + const mockStore = { + loadOAuthSignIn: jest.fn(), + } as unknown as jest.Mocked; + + afterEach(() => { + mockStore.loadOAuthSignIn.mockReset(); + mockCompleteOAuthSignOut.mockClear(); + mockOAuthSignOutRedirect.mockClear(); + }); + + it('should complete OAuth sign out and redirect', async () => { + mockStore.loadOAuthSignIn.mockResolvedValue({ + isOAuthSignIn: true, + preferPrivateSession: false, + }); + await handleOAuthSignOut(cognitoConfig, mockStore); + + expect(mockCompleteOAuthSignOut).toBeCalledWith(mockStore); + expect(mockOAuthSignOutRedirect).toBeCalledWith(cognitoConfig); + }); + + it('should complete OAuth sign out but not redirect', async () => { + mockStore.loadOAuthSignIn.mockResolvedValue({ + isOAuthSignIn: false, + preferPrivateSession: false, + }); + await handleOAuthSignOut(cognitoConfig, mockStore); + + expect(mockCompleteOAuthSignOut).toBeCalledWith(mockStore); + expect(mockOAuthSignOutRedirect).not.toBeCalled(); + }); +}); diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/oAuthSignOutRedirect.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/oAuthSignOutRedirect.test.ts new file mode 100644 index 00000000000..11cf7efc89f --- /dev/null +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/oAuthSignOutRedirect.test.ts @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { oAuthSignOutRedirect } from '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect'; +import { getRedirectUrl } from '../../../../../src/providers/cognito/utils/oauth/getRedirectUrl'; +import { openAuthSession } from '../../../../../src/utils'; + +jest.mock('../../../../../src/providers/cognito/utils/oauth/getRedirectUrl'); +jest.mock('../../../../../src/utils'); + +describe('oAuthSignOutRedirect', () => { + const userPoolClientId = '111111-aaaaa-42d8-891d-ee81a1549398'; + const domain = 'hosted-ui.test'; + const signOutRedirectUrl = 'https://myapp.test/completeSignOut/'; + const encodedSignOutRedirectUrl = + 'https%3A%2F%2Fmyapp.test%2FcompleteSignOut%2F'; + const region = 'us-west-2'; + const authConfig = { + userPoolClientId, + userPoolId: `${region}_zzzzz`, + identityPoolId: `${region}:xxxxxx`, + loginWith: { + oauth: { + domain, + redirectSignIn: ['https://myapp.test/completeSignIn/'], + redirectSignOut: [signOutRedirectUrl], + responseType: 'code' as 'code', // assert string union instead of string type + scopes: [], + }, + }, + }; + // assert mocks + const mockGetRedirectUrl = getRedirectUrl as jest.Mock; + const mockOpenAuthSession = openAuthSession as jest.Mock; + + beforeAll(() => { + mockGetRedirectUrl.mockReturnValue(signOutRedirectUrl); + }); + + afterEach(() => { + mockGetRedirectUrl.mockClear(); + mockOpenAuthSession.mockClear(); + }); + + it('should construct the OAuth logout endpoint and open an auth session to it', async () => { + await oAuthSignOutRedirect(authConfig); + + expect(mockGetRedirectUrl).toBeCalledWith( + authConfig.loginWith.oauth.redirectSignOut + ); + expect(mockOpenAuthSession).toBeCalledWith( + `https://${domain}/logout?client_id=${userPoolClientId}&logout_uri=${encodedSignOutRedirectUrl}`, + authConfig.loginWith.oauth.redirectSignOut, + false + ); + }); + + it('should allow preferPrivateSession to be set', async () => { + await oAuthSignOutRedirect(authConfig, true); + + expect(mockOpenAuthSession).toBeCalledWith( + `https://${domain}/logout?client_id=${userPoolClientId}&logout_uri=${encodedSignOutRedirectUrl}`, + authConfig.loginWith.oauth.redirectSignOut, + true + ); + }); +}); diff --git a/packages/auth/src/providers/cognito/apis/signOut.ts b/packages/auth/src/providers/cognito/apis/signOut.ts index 5c801b14748..6453d073202 100644 --- a/packages/auth/src/providers/cognito/apis/signOut.ts +++ b/packages/auth/src/providers/cognito/apis/signOut.ts @@ -5,6 +5,7 @@ import { Amplify, clearCredentials, CognitoUserPoolConfig, + ConsoleLogger, defaultStorage, Hub, } from '@aws-amplify/core'; @@ -33,6 +34,8 @@ import { DefaultOAuthStore } from '../utils/signInWithRedirectStore'; import { AuthError } from '../../../errors/AuthError'; import { OAUTH_SIGNOUT_EXCEPTION } from '../../../errors/constants'; +const logger = new ConsoleLogger('Auth'); + /** * Signs a user out * @@ -96,26 +99,30 @@ async function clientSignOut(cognitoConfig: CognitoUserPoolConfig) { } } catch (err) { // this shouldn't throw - // TODO(v6): add logger message + logger.debug( + 'Client signOut error caught but will proceed with token removal' + ); } } async function globalSignOut(cognitoConfig: CognitoUserPoolConfig) { try { - const tokens = await tokenOrchestrator.getTokenStore().loadTokens(); - assertAuthTokens(tokens); + const authTokens = await tokenOrchestrator.getTokenStore().loadTokens(); + assertAuthTokens(authTokens); await globalSignOutClient( { region: getRegion(cognitoConfig.userPoolId), userAgentValue: getAuthUserAgentValue(AuthAction.SignOut), }, { - AccessToken: tokens.accessToken.toString(), + AccessToken: authTokens.accessToken.toString(), } ); } catch (err) { // it should not throw - // TODO(v6): add logger + logger.debug( + 'Global signOut error caught but will proceed with token removal' + ); } } diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 9d94264fca3..e0ea8755bee 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -439,7 +439,7 @@ "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "27.40 kB" + "limit": "27.44 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)",