Skip to content

Commit

Permalink
fix(auth): Do not sign out client if Oauth signout fails (#12520)
Browse files Browse the repository at this point in the history
* fix(auth): Do not sign out client if Oauth signout fails

* Do not block local data/tokens clearing for private sessions

* Update to perform slightly different OAuth handling on web vs native

---------

Co-authored-by: Jim Blanchard <[email protected]>
  • Loading branch information
cshfang and jimblanc authored Nov 8, 2023
1 parent 903a012 commit 5f52c64
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 76 deletions.
1 change: 1 addition & 0 deletions packages/auth/src/errors/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export const invalidOriginException = new AuthError({
'redirect is coming from a different origin. The oauth flow needs to be initiated from the same origin',
recoverySuggestion: 'Please call signInWithRedirect from the same origin.',
});
export const OAUTH_SIGNOUT_EXCEPTION = 'OAuthSignOutException';
112 changes: 36 additions & 76 deletions packages/auth/src/providers/cognito/apis/signOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

import {
Amplify,
CognitoUserPoolConfig,
Hub,
clearCredentials,
CognitoUserPoolConfig,
defaultStorage,
Hub,
} from '@aws-amplify/core';
import { getAuthUserAgentValue, openAuthSession } from '../../../utils';

import { getAuthUserAgentValue } from '../../../utils';
import { SignOutInput } from '../types';
import { DefaultOAuthStore } from '../utils/signInWithRedirectStore';
import { tokenOrchestrator } from '../tokenProvider';
import {
AuthAction,
Expand All @@ -28,7 +28,10 @@ import {
assertAuthTokens,
assertAuthTokensWithRefreshToken,
} from '../utils/types';
import { getRedirectUrl } from '../utils/oauth/getRedirectUrl';
import { handleOAuthSignOut } from '../utils/oauth';
import { DefaultOAuthStore } from '../utils/signInWithRedirectStore';
import { AuthError } from '../../../errors/AuthError';
import { OAUTH_SIGNOUT_EXCEPTION } from '../../../errors/constants';

/**
* Signs a user out
Expand All @@ -46,7 +49,33 @@ export async function signOut(input?: SignOutInput): Promise<void> {
await clientSignOut(cognitoConfig);
}

Hub.dispatch('auth', { event: 'signedOut' }, 'Auth', AMPLIFY_SYMBOL);
let hasOAuthConfig;

try {
assertOAuthConfig(cognitoConfig);
hasOAuthConfig = true;
} catch (err) {
hasOAuthConfig = false;
}

if (hasOAuthConfig) {
const oAuthStore = new DefaultOAuthStore(defaultStorage);
oAuthStore.setAuthConfig(cognitoConfig);
const { type } =
(await handleOAuthSignOut(cognitoConfig, oAuthStore)) ?? {};
if (type === 'error') {
throw new AuthError({
name: OAUTH_SIGNOUT_EXCEPTION,
message:
'An error occurred when attempting to log out from OAuth provider.',
});
}
} else {
// complete sign out
tokenOrchestrator.clearTokens();
await clearCredentials();
Hub.dispatch('auth', { event: 'signedOut' }, 'Auth', AMPLIFY_SYMBOL);
}
}

async function clientSignOut(cognitoConfig: CognitoUserPoolConfig) {
Expand All @@ -65,14 +94,9 @@ async function clientSignOut(cognitoConfig: CognitoUserPoolConfig) {
}
);
}

await handleOAuthSignOut(cognitoConfig);
} catch (err) {
// this shouldn't throw
// TODO(v6): add logger message
} finally {
tokenOrchestrator.clearTokens();
await clearCredentials();
}
}

Expand All @@ -89,74 +113,10 @@ async function globalSignOut(cognitoConfig: CognitoUserPoolConfig) {
AccessToken: tokens.accessToken.toString(),
}
);

await handleOAuthSignOut(cognitoConfig);
} catch (err) {
// it should not throw
// TODO(v6): add logger
} finally {
tokenOrchestrator.clearTokens();
await clearCredentials();
}
}

async function handleOAuthSignOut(cognitoConfig: CognitoUserPoolConfig) {
try {
assertOAuthConfig(cognitoConfig);
} catch (err) {
// all good no oauth handling
return;
}

const oauthStore = new DefaultOAuthStore(defaultStorage);
oauthStore.setAuthConfig(cognitoConfig);
const { isOAuthSignIn, preferPrivateSession } =
await oauthStore.loadOAuthSignIn();
await oauthStore.clearOAuthData();

if (isOAuthSignIn) {
oAuthSignOutRedirect(cognitoConfig, preferPrivateSession);
}
}

async function oAuthSignOutRedirect(
authConfig: CognitoUserPoolConfig,
preferPrivateSession: boolean
) {
assertOAuthConfig(authConfig);
const { loginWith, userPoolClientId } = authConfig;
const { domain, redirectSignOut } = loginWith.oauth;
const signoutUri = getRedirectUrl(redirectSignOut);
const oAuthLogoutEndpoint = `https://${domain}/logout?${Object.entries({
client_id: userPoolClientId,
logout_uri: encodeURIComponent(signoutUri),
})
.map(([k, v]) => `${k}=${v}`)
.join('&')}`;

// dispatchAuthEvent(
// 'oAuthSignOut',
// { oAuth: 'signOut' },
// `Signing out from ${oAuthLogoutEndpoint}`
// );
// logger.debug(`Signing out from ${oAuthLogoutEndpoint}`);

await openAuthSession(
oAuthLogoutEndpoint,
redirectSignOut,
preferPrivateSession
);
}

function isSessionRevocable(token: JWT) {
if (token) {
try {
const { origin_jti } = token.payload;
return !!origin_jti;
} catch (err) {
// Nothing to do, token doesnt have origin_jti claim
}
}

return false;
}
const isSessionRevocable = (token: JWT) => !!token?.payload?.origin_jti;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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 { DefaultOAuthStore } from '../../utils/signInWithRedirectStore';
import { tokenOrchestrator } from '../../tokenProvider';

export const completeOAuthSignOut = async (store: DefaultOAuthStore) => {
await store.clearOAuthData();
tokenOrchestrator.clearTokens();
await clearCredentials();
Hub.dispatch('auth', { event: 'signedOut' }, 'Auth', AMPLIFY_SYMBOL);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { CognitoUserPoolConfig } from '@aws-amplify/core';
import { OpenAuthSessionResult } from '../../../../utils/types';
import { DefaultOAuthStore } from '../../utils/signInWithRedirectStore';
import { completeOAuthSignOut } from './completeOAuthSignOut';
import { oAuthSignOutRedirect } from './oAuthSignOutRedirect';

export const handleOAuthSignOut = async (
cognitoConfig: CognitoUserPoolConfig,
store: DefaultOAuthStore
): Promise<void | OpenAuthSessionResult> => {
const { isOAuthSignIn, preferPrivateSession } = await store.loadOAuthSignIn();

if (isOAuthSignIn) {
const result = await oAuthSignOutRedirect(
cognitoConfig,
preferPrivateSession
);
// If this was a private session, clear data and tokens regardless of what happened with logout
// endpoint. Otherwise, only do so if the logout endpoint was succesfully visited.
const shouldCompleteSignOut =
preferPrivateSession || result?.type === 'success';
if (shouldCompleteSignOut) {
await completeOAuthSignOut(store);
}
return result;
}

return completeOAuthSignOut(store);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { CognitoUserPoolConfig } from '@aws-amplify/core';
import { OpenAuthSessionResult } from '../../../../utils/types';
import { DefaultOAuthStore } from '../../utils/signInWithRedirectStore';
import { completeOAuthSignOut } from './completeOAuthSignOut';
import { oAuthSignOutRedirect } from './oAuthSignOutRedirect';

export const handleOAuthSignOut = async (
cognitoConfig: CognitoUserPoolConfig,
store: DefaultOAuthStore
): Promise<void | OpenAuthSessionResult> => {
const { isOAuthSignIn } = await store.loadOAuthSignIn();

// Clear everything before attempting to visted logout endpoint since the current application
// state could be wiped away on redirect
await completeOAuthSignOut(store);

if (isOAuthSignIn) {
// On web, this will always end up being a void action
return oAuthSignOutRedirect(cognitoConfig);
}
};
1 change: 1 addition & 0 deletions packages/auth/src/providers/cognito/utils/oauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

export { generateCodeVerifier } from './generateCodeVerifier';
export { generateState } from './generateState';
export { handleOAuthSignOut } from './handleOAuthSignOut';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { CognitoUserPoolConfig } from '@aws-amplify/core';
import { assertOAuthConfig } from '@aws-amplify/core/internals/utils';
import { openAuthSession } from '../../../../utils';
import { OpenAuthSessionResult } from '../../../../utils/types';
import { getRedirectUrl } from './getRedirectUrl';

export const oAuthSignOutRedirect = async (
authConfig: CognitoUserPoolConfig,
preferPrivateSession: boolean = false
): Promise<void | OpenAuthSessionResult> => {
assertOAuthConfig(authConfig);
const { loginWith, userPoolClientId } = authConfig;
const { domain, redirectSignOut } = loginWith.oauth;
const signoutUri = getRedirectUrl(redirectSignOut);
const oAuthLogoutEndpoint = `https://${domain}/logout?${Object.entries({
client_id: userPoolClientId,
logout_uri: encodeURIComponent(signoutUri),
})
.map(([k, v]) => `${k}=${v}`)
.join('&')}`;

return openAuthSession(
oAuthLogoutEndpoint,
redirectSignOut,
preferPrivateSession
);
};

0 comments on commit 5f52c64

Please sign in to comment.