fix(ui): overriding user info after login (#17825)

* fix(ui): overriding user info after login

* fix displayName override unnecessarily

* address comments

Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>

* fix MSAL login and refresh token issue

* remove unwanted changes

---------

Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
Chirag Madlani 2024-09-17 19:16:59 +05:30 committed by GitHub
parent ed08b27a41
commit 46550b3a88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 159 additions and 99 deletions

View File

@ -109,7 +109,6 @@
"react-antd-column-resize": "1.0.3", "react-antd-column-resize": "1.0.3",
"react-awesome-query-builder": "5.1.2", "react-awesome-query-builder": "5.1.2",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-context-mutex": "^2.0.0",
"react-dnd": "14.0.2", "react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.2", "react-dnd-html5-backend": "14.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",

View File

@ -14,7 +14,6 @@
import { import {
AuthenticationResult, AuthenticationResult,
InteractionRequiredAuthError, InteractionRequiredAuthError,
InteractionStatus,
} from '@azure/msal-browser'; } from '@azure/msal-browser';
import { useAccount, useMsal } from '@azure/msal-react'; import { useAccount, useMsal } from '@azure/msal-react';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
@ -23,10 +22,8 @@ import React, {
forwardRef, forwardRef,
Fragment, Fragment,
ReactNode, ReactNode,
useEffect,
useImperativeHandle, useImperativeHandle,
} from 'react'; } from 'react';
import { useMutex } from 'react-context-mutex';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useApplicationStore } from '../../../hooks/useApplicationStore';
import { msalLoginRequest } from '../../../utils/AuthProvider.util'; import { msalLoginRequest } from '../../../utils/AuthProvider.util';
@ -50,34 +47,9 @@ const MsalAuthenticator = forwardRef<AuthenticatorRef, Props>(
{ children, onLoginSuccess, onLogoutSuccess, onLoginFailure }: Props, { children, onLoginSuccess, onLogoutSuccess, onLoginFailure }: Props,
ref ref
) => { ) => {
const { setOidcToken, getOidcToken } = useApplicationStore(); const { setOidcToken } = useApplicationStore();
const { instance, accounts, inProgress } = useMsal(); const { instance, accounts } = useMsal();
const account = useAccount(accounts[0] || {}); const account = useAccount(accounts[0] || {});
const MutexRunner = useMutex();
const mutex = new MutexRunner('fetchIdToken');
const handleOnLogoutSuccess = () => {
for (const key in localStorage) {
if (key.includes('-login.windows.net-') || key.startsWith('msal.')) {
localStorage.removeItem(key);
}
}
onLogoutSuccess();
};
const login = () => {
try {
instance.loginPopup(msalLoginRequest);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
onLoginFailure(error as AxiosError);
}
};
const logout = () => {
handleOnLogoutSuccess();
};
const parseResponse = (response: AuthenticationResult): OidcUser => { const parseResponse = (response: AuthenticationResult): OidcUser => {
// Call your API with the access token and return the data you need to save in state // Call your API with the access token and return the data you need to save in state
@ -104,6 +76,29 @@ const MsalAuthenticator = forwardRef<AuthenticatorRef, Props>(
return user; return user;
}; };
const handleOnLogoutSuccess = () => {
for (const key in localStorage) {
if (key.includes('-login.windows.net-') || key.startsWith('msal.')) {
localStorage.removeItem(key);
}
}
onLogoutSuccess();
};
const login = async () => {
try {
const response = await instance.loginPopup(msalLoginRequest);
onLoginSuccess(parseResponse(response));
} catch (error) {
onLoginFailure(error as AxiosError);
}
};
const logout = () => {
handleOnLogoutSuccess();
};
const fetchIdToken = async ( const fetchIdToken = async (
shouldFallbackToPopup = false shouldFallbackToPopup = false
): Promise<OidcUser> => { ): Promise<OidcUser> => {
@ -154,34 +149,11 @@ const MsalAuthenticator = forwardRef<AuthenticatorRef, Props>(
}; };
const renewIdToken = async () => { const renewIdToken = async () => {
const user = await fetchIdToken(); const user = await fetchIdToken(true);
return user.id_token; return user.id_token;
}; };
useEffect(() => {
const oidcUserToken = getOidcToken();
if (
!oidcUserToken &&
inProgress === InteractionStatus.None &&
(accounts.length > 0 || account?.idTokenClaims)
) {
mutex.run(async () => {
mutex.lock();
fetchIdToken(true)
.then((user) => {
if ((user as OidcUser).id_token) {
onLoginSuccess(user as OidcUser);
}
})
.catch(onLoginFailure)
.finally(() => {
mutex.unlock();
});
});
}
}, [inProgress, accounts, instance, account]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
invokeLogin: login, invokeLogin: login,
invokeLogout: logout, invokeLogout: logout,

View File

@ -25,7 +25,6 @@ import {
InternalAxiosRequestConfig, InternalAxiosRequestConfig,
} from 'axios'; } from 'axios';
import { CookieStorage } from 'cookie-storage'; import { CookieStorage } from 'cookie-storage';
import { compare } from 'fast-json-patch';
import { isEmpty, isNil, isNumber } from 'lodash'; import { isEmpty, isNil, isNumber } from 'lodash';
import Qs from 'qs'; import Qs from 'qs';
import React, { import React, {
@ -63,7 +62,7 @@ import {
fetchAuthenticationConfig, fetchAuthenticationConfig,
fetchAuthorizerConfig, fetchAuthorizerConfig,
} from '../../../rest/miscAPI'; } from '../../../rest/miscAPI';
import { getLoggedInUser, updateUserDetail } from '../../../rest/userAPI'; import { getLoggedInUser } from '../../../rest/userAPI';
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil'; import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
import { import {
extractDetailsFromToken, extractDetailsFromToken,
@ -76,10 +75,7 @@ import {
import { getPathNameFromWindowLocation } from '../../../utils/RouterUtils'; import { getPathNameFromWindowLocation } from '../../../utils/RouterUtils';
import { escapeESReservedCharacters } from '../../../utils/StringsUtils'; import { escapeESReservedCharacters } from '../../../utils/StringsUtils';
import { showErrorToast, showInfoToast } from '../../../utils/ToastUtils'; import { showErrorToast, showInfoToast } from '../../../utils/ToastUtils';
import { import { checkIfUpdateRequired } from '../../../utils/UserDataUtils';
getUserDataFromOidc,
matchUserDetails,
} from '../../../utils/UserDataUtils';
import { resetWebAnalyticSession } from '../../../utils/WebAnalyticsUtils'; import { resetWebAnalyticSession } from '../../../utils/WebAnalyticsUtils';
import Loader from '../../common/Loader/Loader'; import Loader from '../../common/Loader/Loader';
import Auth0Authenticator from '../AppAuthenticators/Auth0Authenticator'; import Auth0Authenticator from '../AppAuthenticators/Auth0Authenticator';
@ -269,29 +265,6 @@ export const AuthProvider = ({
} }
}; };
const getUpdatedUser = async (updatedData: User, existingData: User) => {
// PUT method for users api only excepts below fields
const updatedUserData = { ...existingData, ...updatedData };
const jsonPatch = compare(existingData, updatedUserData);
try {
const res = await updateUserDetail(existingData.id, jsonPatch);
if (res) {
setCurrentUser({ ...existingData, ...res });
} else {
throw t('server.unexpected-response');
}
} catch (error) {
setCurrentUser(existingData);
showErrorToast(
error as AxiosError,
t('server.entity-updating-error', {
entity: t('label.admin-profile'),
})
);
}
};
/** /**
* Renew Id Token handler for all the SSOs. * Renew Id Token handler for all the SSOs.
* This method will be called when the id token is about to expire. * This method will be called when the id token is about to expire.
@ -428,12 +401,8 @@ export const AuthProvider = ({
const res = await getLoggedInUser({ fields }); const res = await getLoggedInUser({ fields });
if (res) { if (res) {
const updatedUserData = getUserDataFromOidc(res, newUser); const userDetails = await checkIfUpdateRequired(res, newUser);
if (!matchUserDetails(res, updatedUserData, ['profile', 'email'])) { setCurrentUser(userDetails);
getUpdatedUser(updatedUserData, res);
} else {
setCurrentUser(res);
}
// Fetch domains at the start // Fetch domains at the start
await fetchDomainList(); await fetchDomainList();

View File

@ -47,7 +47,7 @@ type ListMenuItemProps = {
listItems: EntityReference[]; listItems: EntityReference[];
labelRenderer: (item: EntityReference) => ReactNode; labelRenderer: (item: EntityReference) => ReactNode;
readMoreLabelRenderer: (count: number) => ReactNode; readMoreLabelRenderer: (count: number) => ReactNode;
readMoreKey?: string; readMoreKey: string;
sizeLimit?: number; sizeLimit?: number;
}; };
@ -66,7 +66,7 @@ const renderLimitedListMenuItem = ({
const items = listItems.slice(0, sizeLimit); const items = listItems.slice(0, sizeLimit);
return isEmpty(items) return isEmpty(items)
? [{ label: NO_DATA_PLACEHOLDER, key: 'no-teams' }] ? [{ label: NO_DATA_PLACEHOLDER, key: readMoreKey.replace('more', 'no') }]
: [ : [
...(items?.map((item) => ({ ...(items?.map((item) => ({
label: labelRenderer(item), label: labelRenderer(item),

View File

@ -73,6 +73,7 @@ class TokenService {
response = await this.renewToken(); response = await this.renewToken();
this.tokeUpdateInProgress = false; this.tokeUpdateInProgress = false;
} catch (error) { } catch (error) {
useApplicationStore.getState().onLogoutHandler();
// Do nothing // Do nothing
} finally { } finally {
this.tokeUpdateInProgress = false; this.tokeUpdateInProgress = false;

View File

@ -10,8 +10,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { OidcUser } from '../components/Auth/AuthProviders/AuthProvider.interface';
import { User } from '../generated/entity/teams/user'; import { User } from '../generated/entity/teams/user';
import { getUserWithImage } from './UserDataUtils'; import { checkIfUpdateRequired, getUserWithImage } from './UserDataUtils';
describe('getUserWithImage', () => { describe('getUserWithImage', () => {
it('should return the correct user based on profile and isBot status', () => { it('should return the correct user based on profile and isBot status', () => {
@ -88,3 +89,64 @@ describe('getUserWithImage', () => {
expect(result).toEqual(userWithoutProfile); expect(result).toEqual(userWithoutProfile);
}); });
}); });
describe('checkIfUpdateRequired', () => {
it('should return the updated user details if update is required', async () => {
const existingUserDetails: User = {
email: 'a@a.com',
id: '1',
name: 'user',
isBot: false,
};
const newUser: OidcUser = {
id_token: 'idToken',
scope: 'scope',
profile: {
email: 'a@a.com',
name: 'updatedUser',
picture: '',
preferred_username: 'preferred_username',
sub: 'sub',
},
};
const updatedUserDetails = await checkIfUpdateRequired(
existingUserDetails,
newUser
);
expect(updatedUserDetails).toEqual({
...existingUserDetails,
name: 'user',
});
});
it('should return the existing user details if update is not required', async () => {
const existingUserDetails: User = {
email: 'a@a.com',
id: '1',
name: 'user',
isBot: false,
};
const newUser: OidcUser = {
id_token: 'idToken',
scope: 'scope',
profile: {
email: 'a@a.com',
name: 'user',
picture: '',
preferred_username: 'preferred_username',
sub: 'sub',
},
};
const updatedUserDetails = await checkIfUpdateRequired(
existingUserDetails,
newUser
);
expect(updatedUserDetails).toEqual(existingUserDetails);
});
});

View File

@ -11,6 +11,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { OidcUser } from '../components/Auth/AuthProviders/AuthProvider.interface'; import { OidcUser } from '../components/Auth/AuthProviders/AuthProvider.interface';
import { WILD_CARD_CHAR } from '../constants/char.constants'; import { WILD_CARD_CHAR } from '../constants/char.constants';
@ -18,13 +20,16 @@ import { SettledStatus } from '../enums/Axios.enum';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
import { SearchResponse } from '../interface/search.interface'; import { SearchResponse } from '../interface/search.interface';
import { getSearchedTeams, getSearchedUsers } from '../rest/miscAPI'; import { getSearchedTeams, getSearchedUsers } from '../rest/miscAPI';
import { updateUserDetail } from '../rest/userAPI';
import { User } from './../generated/entity/teams/user'; import { User } from './../generated/entity/teams/user';
import { formatTeamsResponse, formatUsersResponse } from './APIUtils'; import { formatTeamsResponse, formatUsersResponse } from './APIUtils';
import { getImages } from './CommonUtils'; import { getImages } from './CommonUtils';
import i18n from './i18next/LocalUtil';
import { import {
getImageWithResolutionAndFallback, getImageWithResolutionAndFallback,
ImageQuality, ImageQuality,
} from './ProfilerUtils'; } from './ProfilerUtils';
import { showErrorToast } from './ToastUtils';
import userClassBase from './UserClassBase'; import userClassBase from './UserClassBase';
export const getUserDataFromOidc = ( export const getUserDataFromOidc = (
@ -44,7 +49,7 @@ export const getUserDataFromOidc = (
...userData, ...userData,
email, email,
displayName: oidcUser.profile.name, displayName: oidcUser.profile.name,
profile: images ? { images } : userData.profile, profile: images ? { ...userData.profile, images } : undefined,
}; };
}; };
@ -124,3 +129,44 @@ export const getUserWithImage = (user: User) => {
return user; return user;
}; };
export const checkIfUpdateRequired = async (
existingUserDetails: User,
newUser: OidcUser
): Promise<User> => {
const updatedUserData = getUserDataFromOidc(existingUserDetails, newUser);
if (existingUserDetails.email !== updatedUserData.email) {
return existingUserDetails;
} else if (
existingUserDetails.email === updatedUserData.email &&
// We only want to update images / profile info not any other information
updatedUserData.profile?.images &&
!matchUserDetails(existingUserDetails, updatedUserData, ['profile'])
) {
const finalData = {
...existingUserDetails,
// We want to override any profile information that is coming from the OIDC provider
profile: {
...existingUserDetails.profile,
...updatedUserData.profile
},
};
const jsonPatch = compare(existingUserDetails, finalData);
try {
const res = await updateUserDetail(existingUserDetails.id, jsonPatch);
return res;
} catch (error) {
showErrorToast(
error as AxiosError,
i18n.t('server.entity-updating-error', {
entity: i18n.t('label.admin-profile'),
})
);
}
}
return existingUserDetails;
};

View File

@ -205,6 +205,14 @@ module.exports = {
from: path.join(__dirname, 'public/favicon.png'), from: path.join(__dirname, 'public/favicon.png'),
to: outputPath, to: outputPath,
}, },
{
from: path.join(__dirname, 'public/favicons/favicon-16x16.png'),
to: outputPath,
},
{
from: path.join(__dirname, 'public/favicons/favicon-32x32.png'),
to: outputPath,
},
{ {
from: path.join(__dirname, 'public/logo192.png'), from: path.join(__dirname, 'public/logo192.png'),
to: outputPath, to: outputPath,

View File

@ -199,6 +199,14 @@ module.exports = {
from: path.join(__dirname, 'public/favicon.png'), from: path.join(__dirname, 'public/favicon.png'),
to: outputPath, to: outputPath,
}, },
{
from: path.join(__dirname, 'public/favicons/favicon-16x16.png'),
to: outputPath,
},
{
from: path.join(__dirname, 'public/favicons/favicon-32x32.png'),
to: outputPath,
},
{ {
from: path.join(__dirname, 'public/logo192.png'), from: path.join(__dirname, 'public/logo192.png'),
to: outputPath, to: outputPath,

View File

@ -12186,11 +12186,6 @@ react-codemirror2@^7.2.1:
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-7.2.1.tgz#38dab492fcbe5fb8ebf5630e5bb7922db8d3a10c" resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-7.2.1.tgz#38dab492fcbe5fb8ebf5630e5bb7922db8d3a10c"
integrity sha512-t7YFmz1AXdlImgHXA9Ja0T6AWuopilub24jRaQdPVbzUJVNKIYuy3uCFZYa7CE5S3UW6SrSa5nAqVQvtzRF9gw== integrity sha512-t7YFmz1AXdlImgHXA9Ja0T6AWuopilub24jRaQdPVbzUJVNKIYuy3uCFZYa7CE5S3UW6SrSa5nAqVQvtzRF9gw==
react-context-mutex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-context-mutex/-/react-context-mutex-2.0.0.tgz#4060bc8fbced6d2680ab093139c727201d402b88"
integrity sha512-Qss0YfJAKQ/+CYdVjHB3w7wRLdeetSrp2IMEEouXEpoTdohaCXIKZtFyNMMKDytLVWqHiiXg0RDA2kj/DQbeMQ==
react-dnd-html5-backend@14.0.2: react-dnd-html5-backend@14.0.2:
version "14.0.2" version "14.0.2"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz#25019388f6abdeeda3a6fea835dff155abb2085c" resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.2.tgz#25019388f6abdeeda3a6fea835dff155abb2085c"