diff --git a/packages/auth/__tests__/providers/cognito/signOut.test.ts b/packages/auth/__tests__/providers/cognito/signOut.test.ts index adae8c494cc..e7003463f4e 100644 --- a/packages/auth/__tests__/providers/cognito/signOut.test.ts +++ b/packages/auth/__tests__/providers/cognito/signOut.test.ts @@ -220,6 +220,7 @@ describe('signOut', () => { expect(mockHandleOAuthSignOut).toHaveBeenCalledWith( cognitoConfigWithOauth, mockDefaultOAuthStoreInstance, + mockTokenOrchestrator, ); // 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. diff --git a/packages/auth/__tests__/providers/cognito/tokenOrchestrator.test.ts b/packages/auth/__tests__/providers/cognito/tokenOrchestrator.test.ts index 5a31dbf0c74..8906d8d7eed 100644 --- a/packages/auth/__tests__/providers/cognito/tokenOrchestrator.test.ts +++ b/packages/auth/__tests__/providers/cognito/tokenOrchestrator.test.ts @@ -25,6 +25,8 @@ const mockAuthTokenStore = { setKeyValueStorage: jest.fn(), getDeviceMetadata: jest.fn(), clearDeviceMetadata: jest.fn(), + setOAuthMetadata: jest.fn(), + getOAuthMetadata: jest.fn(), }; const mockTokenRefresher = jest.fn(); const validAuthConfig: ResourcesConfig = { diff --git a/packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts b/packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts index e1c25ec86f7..c0853b51f23 100644 --- a/packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts +++ b/packages/auth/__tests__/providers/cognito/tokenProvider/tokenOrchestrator.test.ts @@ -24,6 +24,8 @@ describe('tokenOrchestrator', () => { setKeyValueStorage: jest.fn(), getDeviceMetadata: jest.fn(), clearDeviceMetadata: jest.fn(), + getOAuthMetadata: jest.fn(), + setOAuthMetadata: jest.fn(), }; beforeAll(() => { diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthFlow.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthFlow.test.ts index 78e95120977..8d62c014a94 100644 --- a/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthFlow.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/completeOAuthFlow.test.ts @@ -13,6 +13,7 @@ import { AuthErrorTypes } from '../../../../../src/types/Auth'; import { OAuthStore } from '../../../../../src/providers/cognito/utils/types'; import { completeOAuthFlow } from '../../../../../src/providers/cognito/utils/oauth/completeOAuthFlow'; +jest.mock('../../../../../src/providers/cognito/tokenProvider'); jest.mock('@aws-amplify/core', () => ({ Hub: { dispatch: jest.fn(), diff --git a/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts index f6fab30158f..1ce83d076ed 100644 --- a/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts +++ b/packages/auth/__tests__/providers/cognito/utils/oauth/handleOAuthSignOut.test.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { TokenOrchestrator } from '../../../../../src/providers/cognito'; 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'; @@ -12,6 +13,7 @@ jest.mock( jest.mock( '../../../../../src/providers/cognito/utils/oauth/oAuthSignOutRedirect', ); +jest.mock('../../../../../src/providers/cognito/tokenProvider'); describe('handleOAuthSignOut', () => { const region = 'us-west-2'; @@ -27,9 +29,13 @@ describe('handleOAuthSignOut', () => { const mockStore = { loadOAuthSignIn: jest.fn(), } as unknown as jest.Mocked; + const mockTokenOrchestrator = { + getOAuthMetadata: jest.fn(), + } as unknown as jest.Mocked; afterEach(() => { mockStore.loadOAuthSignIn.mockReset(); + mockTokenOrchestrator.getOAuthMetadata.mockReset(); mockCompleteOAuthSignOut.mockClear(); mockOAuthSignOutRedirect.mockClear(); }); @@ -39,7 +45,21 @@ describe('handleOAuthSignOut', () => { isOAuthSignIn: true, preferPrivateSession: false, }); - await handleOAuthSignOut(cognitoConfig, mockStore); + await handleOAuthSignOut(cognitoConfig, mockStore, mockTokenOrchestrator); + + expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); + expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(cognitoConfig); + }); + + it('should complete OAuth sign out and redirect when there oauth metadata in tokenOrchestrator', async () => { + mockTokenOrchestrator.getOAuthMetadata.mockResolvedValue({ + oauthSignIn: true, + }); + mockStore.loadOAuthSignIn.mockResolvedValue({ + isOAuthSignIn: false, + preferPrivateSession: false, + }); + await handleOAuthSignOut(cognitoConfig, mockStore, mockTokenOrchestrator); expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); expect(mockOAuthSignOutRedirect).toHaveBeenCalledWith(cognitoConfig); @@ -50,7 +70,7 @@ describe('handleOAuthSignOut', () => { isOAuthSignIn: false, preferPrivateSession: false, }); - await handleOAuthSignOut(cognitoConfig, mockStore); + await handleOAuthSignOut(cognitoConfig, mockStore, mockTokenOrchestrator); expect(mockCompleteOAuthSignOut).toHaveBeenCalledWith(mockStore); expect(mockOAuthSignOutRedirect).not.toHaveBeenCalled(); diff --git a/packages/auth/src/providers/cognito/apis/signOut.ts b/packages/auth/src/providers/cognito/apis/signOut.ts index 65be0162927..fc98d3957f4 100644 --- a/packages/auth/src/providers/cognito/apis/signOut.ts +++ b/packages/auth/src/providers/cognito/apis/signOut.ts @@ -65,7 +65,11 @@ export async function signOut(input?: SignOutInput): Promise { const oAuthStore = new DefaultOAuthStore(defaultStorage); oAuthStore.setAuthConfig(cognitoConfig); const { type } = - (await handleOAuthSignOut(cognitoConfig, oAuthStore)) ?? {}; + (await handleOAuthSignOut( + cognitoConfig, + oAuthStore, + tokenOrchestrator, + )) ?? {}; if (type === 'error') { throw new AuthError({ name: OAUTH_SIGNOUT_EXCEPTION, diff --git a/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts b/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts index 0e16a021969..121875013e2 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/TokenOrchestrator.ts @@ -25,6 +25,7 @@ import { AuthTokenStore, CognitoAuthTokens, DeviceMetadata, + OAuthMetadata, TokenRefresher, } from './types'; @@ -203,4 +204,12 @@ export class TokenOrchestrator implements AuthTokenOrchestrator { clearDeviceMetadata(username?: string): Promise { return this.getTokenStore().clearDeviceMetadata(username); } + + setOAuthMetadata(metadata: OAuthMetadata): Promise { + return this.getTokenStore().setOAuthMetadata(metadata); + } + + getOAuthMetadata(): Promise { + return this.getTokenStore().getOAuthMetadata(); + } } diff --git a/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts b/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts index 53ac3228d85..74d6b9400c6 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/TokenStore.ts @@ -14,6 +14,7 @@ import { AuthTokenStore, CognitoAuthTokens, DeviceMetadata, + OAuthMetadata, } from './types'; import { TokenProviderErrorCode, assert } from './errorHelpers'; @@ -163,6 +164,7 @@ export class DefaultTokenStore implements AuthTokenStore { this.getKeyValueStorage().removeItem(authKeys.refreshToken), this.getKeyValueStorage().removeItem(authKeys.signInDetails), this.getKeyValueStorage().removeItem(this.getLastAuthUserKey()), + this.getKeyValueStorage().removeItem(authKeys.oauthMetadata), ]); } @@ -222,6 +224,22 @@ export class DefaultTokenStore implements AuthTokenStore { return lastAuthUser; } + + async setOAuthMetadata(metadata: OAuthMetadata): Promise { + const { oauthMetadata: oauthMetadataKey } = await this.getAuthKeys(); + await this.getKeyValueStorage().setItem( + oauthMetadataKey, + JSON.stringify(metadata), + ); + } + + async getOAuthMetadata(): Promise { + const { oauthMetadata: oauthMetadataKey } = await this.getAuthKeys(); + const oauthMetadata = + await this.getKeyValueStorage().getItem(oauthMetadataKey); + + return oauthMetadata && JSON.parse(oauthMetadata); + } } export const createKeysForAuthStorage = ( diff --git a/packages/auth/src/providers/cognito/tokenProvider/types.ts b/packages/auth/src/providers/cognito/tokenProvider/types.ts index f58483a334b..5db7c62f012 100644 --- a/packages/auth/src/providers/cognito/tokenProvider/types.ts +++ b/packages/auth/src/providers/cognito/tokenProvider/types.ts @@ -34,6 +34,7 @@ export const AuthTokenStorageKeys = { randomPasswordKey: 'randomPasswordKey', deviceGroupKey: 'deviceGroupKey', signInDetails: 'signInDetails', + oauthMetadata: 'oauthMetadata', }; export interface AuthTokenStore { @@ -44,6 +45,8 @@ export interface AuthTokenStore { setKeyValueStorage(keyValueStorage: KeyValueStorageInterface): void; getDeviceMetadata(username?: string): Promise; clearDeviceMetadata(username?: string): Promise; + setOAuthMetadata(metadata: OAuthMetadata): Promise; + getOAuthMetadata(): Promise; } export interface AuthTokenOrchestrator { @@ -58,6 +61,8 @@ export interface AuthTokenOrchestrator { clearTokens(): Promise; getDeviceMetadata(username?: string): Promise; clearDeviceMetadata(username?: string): Promise; + setOAuthMetadata(metadata: OAuthMetadata): Promise; + getOAuthMetadata(): Promise; } export interface CognitoUserPoolTokenProviderType extends TokenProvider { @@ -78,3 +83,7 @@ export interface DeviceMetadata { deviceGroupKey?: string; randomPassword: string; } + +export interface OAuthMetadata { + oauthSignIn: boolean; +} diff --git a/packages/auth/src/providers/cognito/utils/oauth/completeOAuthFlow.ts b/packages/auth/src/providers/cognito/utils/oauth/completeOAuthFlow.ts index d9ebc5976a8..f374ed98156 100644 --- a/packages/auth/src/providers/cognito/utils/oauth/completeOAuthFlow.ts +++ b/packages/auth/src/providers/cognito/utils/oauth/completeOAuthFlow.ts @@ -11,6 +11,7 @@ import { Hub, decodeJWT } from '@aws-amplify/core'; import { cacheCognitoTokens } from '../../tokenProvider/cacheTokens'; import { dispatchSignedInHubEvent } from '../dispatchSignedInHubEvent'; +import { tokenOrchestrator } from '../../tokenProvider'; import { createOAuthError } from './createOAuthError'; import { resolveAndClearInflightPromises } from './inflightPromise'; @@ -227,6 +228,9 @@ const completeFlow = async ({ redirectUri: string; state: string; }) => { + await tokenOrchestrator.setOAuthMetadata({ + oauthSignIn: true, + }); await oAuthStore.clearOAuthData(); await oAuthStore.storeOAuthSignIn(true, preferPrivateSession); diff --git a/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.ts b/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.ts index 0e16008e752..ecb09f23bc5 100644 --- a/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.ts +++ b/packages/auth/src/providers/cognito/utils/oauth/handleOAuthSignOut.ts @@ -5,6 +5,7 @@ import { CognitoUserPoolConfig } from '@aws-amplify/core'; import { OpenAuthSessionResult } from '../../../../utils/types'; import { DefaultOAuthStore } from '../../utils/signInWithRedirectStore'; +import { TokenOrchestrator } from '../../tokenProvider'; import { completeOAuthSignOut } from './completeOAuthSignOut'; import { oAuthSignOutRedirect } from './oAuthSignOutRedirect'; @@ -12,14 +13,22 @@ import { oAuthSignOutRedirect } from './oAuthSignOutRedirect'; export const handleOAuthSignOut = async ( cognitoConfig: CognitoUserPoolConfig, store: DefaultOAuthStore, + tokenOrchestrator: TokenOrchestrator, ): Promise => { const { isOAuthSignIn } = await store.loadOAuthSignIn(); + const oauthMetadata = await tokenOrchestrator.getOAuthMetadata(); // Clear everything before attempting to visted logout endpoint since the current application // state could be wiped away on redirect await completeOAuthSignOut(store); - if (isOAuthSignIn) { + // The isOAuthSignIn flag is propagated by the oAuthToken store which manages oauth keys in local storage only. + // These keys are used to determine if a user is in an inflight or signedIn oauth states. + // However, this behavior represents an issue when 2 apps share the same set of tokens in Cookie storage because the app that didn't + // start the OAuth will not have access to the oauth keys. + // A heuristic solution is to add oauth metadata to the tokenOrchestrator which will have access to the underlying + // storage mechanism that is used by Amplify. + if (isOAuthSignIn || oauthMetadata?.oauthSignIn) { // On web, this will always end up being a void action return oAuthSignOutRedirect(cognitoConfig); }