fix(ui): JwtClaim should also check for "sub" claim and add fallback mechanism (#9909)

* fix(ui): JwtClaim should also check for "sub" claim and add fallback mechanism

* test: add unit tests
This commit is contained in:
Sachin Chaurasiya 2023-01-25 18:50:39 +05:30 committed by GitHub
parent aa315a2e85
commit 1cfdd6d7b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 133 additions and 15 deletions

View File

@ -24,7 +24,7 @@ export type UserProfile = {
name: string; name: string;
picture: string; picture: string;
locale?: string; locale?: string;
} & Pick<Profile, 'preferred_username'>; } & Pick<Profile, 'preferred_username' | 'sub'>;
export type OidcUser = { export type OidcUser = {
id_token: string; id_token: string;
@ -41,4 +41,5 @@ export interface AuthenticatorRef {
export enum JWT_PRINCIPAL_CLAIMS { export enum JWT_PRINCIPAL_CLAIMS {
EMAIL = 'email', EMAIL = 'email',
PREFERRED_USERNAME = 'preferred_username', PREFERRED_USERNAME = 'preferred_username',
SUB = 'sub',
} }

View File

@ -111,6 +111,7 @@ const BasicAuthProvider = ({
email, email,
name: '', name: '',
picture: '', picture: '',
sub: '',
}, },
scope: '', scope: '',
}); });

View File

@ -59,6 +59,7 @@ export const OktaAuthProvider: FunctionComponent<Props> = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
picture: (info as any).imageUrl || '', picture: (info as any).imageUrl || '',
locale: info.locale || '', locale: info.locale || '',
sub: info.sub,
}, },
}; };
onLoginSuccess(user); onLoginSuccess(user);

View File

@ -83,6 +83,7 @@ const MsalAuthenticator = forwardRef<AuthenticatorRef, Props>(
email: account?.username || '', email: account?.username || '',
name: account?.name || '', name: account?.name || '',
picture: '', picture: '',
sub: '',
}, },
}; };

View File

@ -121,6 +121,7 @@ describe('Test Auth0Callback component', () => {
name: 'test_user', name: 'test_user',
picture: 'test_picture', picture: 'test_picture',
locale: 'test_locale', locale: 'test_locale',
sub: '',
}, },
scope: '', scope: '',
}); });

View File

@ -34,6 +34,7 @@ const Auth0Callback: VFC = () => {
name: user?.name || '', name: user?.name || '',
picture: user?.picture || '', picture: user?.picture || '',
locale: user?.locale || '', locale: user?.locale || '',
sub: user?.sub || '',
}, },
}; };
handleSuccessfulLogin(oidcUser); handleSuccessfulLogin(oidcUser);

View File

@ -19,6 +19,8 @@ import AppState from '../../AppState';
import { getImages } from '../../utils/CommonUtils'; import { getImages } from '../../utils/CommonUtils';
import { mockCreateUser } from './mocks/signup.mock'; import { mockCreateUser } from './mocks/signup.mock';
let letExpectedUserName = 'sample123';
const mockChangeHandler = jest.fn(); const mockChangeHandler = jest.fn();
const mockSubmitHandler = jest.fn(); const mockSubmitHandler = jest.fn();
const mockShowErrorToast = jest.fn(); const mockShowErrorToast = jest.fn();
@ -84,6 +86,10 @@ jest.mock('../../utils/CommonUtils', () => ({
), ),
})); }));
jest.mock('utils/AuthProvider.util', () => ({
getNameFromUserData: jest.fn().mockImplementation(() => letExpectedUserName),
}));
describe('Signup page', () => { describe('Signup page', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@ -222,6 +228,7 @@ describe('Signup page', () => {
it('Handlers in form should work if data is empty', async () => { it('Handlers in form should work if data is empty', async () => {
(getImages as jest.Mock).mockImplementationOnce(() => Promise.reject('')); (getImages as jest.Mock).mockImplementationOnce(() => Promise.reject(''));
letExpectedUserName = '';
AppState.newUser = { AppState.newUser = {
name: '', name: '',

View File

@ -21,12 +21,12 @@ import { CookieStorage } from 'cookie-storage';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { createUser } from 'rest/userAPI'; import { createUser } from 'rest/userAPI';
import { getNameFromUserData } from 'utils/AuthProvider.util';
import appState from '../../AppState'; import appState from '../../AppState';
import { REDIRECT_PATHNAME, ROUTES } from '../../constants/constants'; import { REDIRECT_PATHNAME, ROUTES } from '../../constants/constants';
import { CreateUser } from '../../generated/api/teams/createUser'; import { CreateUser } from '../../generated/api/teams/createUser';
import { User } from '../../generated/entity/teams/user'; import { User } from '../../generated/entity/teams/user';
import jsonData from '../../jsons/en'; import jsonData from '../../jsons/en';
import { getNameFromUserData } from '../../utils/AuthProvider.util';
import { getImages } from '../../utils/CommonUtils'; import { getImages } from '../../utils/CommonUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils'; import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast } from '../../utils/ToastUtils'; import { showErrorToast } from '../../utils/ToastUtils';

View File

@ -18,10 +18,7 @@ import {
PopupRequest, PopupRequest,
PublicClientApplication, PublicClientApplication,
} from '@azure/msal-browser'; } from '@azure/msal-browser';
import { import { UserProfile } from 'components/authentication/auth-provider/AuthProvider.interface';
JWT_PRINCIPAL_CLAIMS,
UserProfile,
} from 'components/authentication/auth-provider/AuthProvider.interface';
import jwtDecode, { JwtPayload } from 'jwt-decode'; import jwtDecode, { JwtPayload } from 'jwt-decode';
import { first, isNil } from 'lodash'; import { first, isNil } from 'lodash';
import { WebStorageStateStore } from 'oidc-client'; import { WebStorageStateStore } from 'oidc-client';
@ -212,18 +209,32 @@ export const getNameFromUserData = (
user: UserProfile, user: UserProfile,
jwtPrincipalClaims: AuthenticationConfiguration['jwtPrincipalClaims'] = [] jwtPrincipalClaims: AuthenticationConfiguration['jwtPrincipalClaims'] = []
) => { ) => {
// get the first claim from claim list // filter and extract the present claims in user profile
const firstClaim = first(jwtPrincipalClaims); const jwtClaims = jwtPrincipalClaims.reduce(
const nameFromEmail = getNameFromEmail(user.email); (prev: string[], curr: string) => {
const currentClaim = user[curr as keyof UserProfile];
if (currentClaim) {
return [...prev, currentClaim];
} else {
return prev;
}
},
[]
);
/* if first claim is preferred_username then return preferred_username // get the first claim from claims list
* for fallback if preferred_username is not present then return the name from email const firstClaim = first(jwtClaims);
*/
if (firstClaim === JWT_PRINCIPAL_CLAIMS.PREFERRED_USERNAME) { let userName = '';
return user.preferred_username ?? nameFromEmail;
// if claims contains the "@" then split it out otherwise assign it to username as it is
if (firstClaim?.includes('@')) {
userName = getNameFromEmail(firstClaim);
} else {
userName = firstClaim ?? '';
} }
return nameFromEmail; return userName;
}; };
export const isProtectedRoute = (pathname: string) => { export const isProtectedRoute = (pathname: string) => {

View File

@ -0,0 +1,94 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getNameFromUserData } from './AuthProvider.util';
const userProfile = {
email: 'testUser@gmail.com',
sub: 'i_am_sub',
preferred_username: 'i_am_preferred_username',
name: 'Test User',
picture: '',
};
describe('Test Auth Provider utils', () => {
it('getNameFromUserData should return the userName for first claim', () => {
// should return the preferred username
const userName1 = getNameFromUserData(userProfile, [
'preferred_username',
'email',
'sub',
]);
expect(userName1).toEqual('i_am_preferred_username');
// should return the name from email
const userName2 = getNameFromUserData(userProfile, [
'email',
'preferred_username',
'sub',
]);
expect(userName2).toEqual('testUser');
// should return the sub
const userName3 = getNameFromUserData(userProfile, [
'sub',
'email',
'preferred_username',
]);
expect(userName3).toEqual('i_am_sub');
});
it('getNameFromUserData should fallback to next claim if first claim is not present', () => {
// should return the name from email as fallback
const userName1 = getNameFromUserData(
{ ...userProfile, preferred_username: '' },
['preferred_username', 'email', 'sub']
);
expect(userName1).toEqual('testUser');
// should return the sub as fallback
const userName2 = getNameFromUserData(
{ ...userProfile, preferred_username: '' },
['preferred_username', 'sub', 'email']
);
expect(userName2).toEqual('i_am_sub');
// should return the name from email as fallback if both 'preferred_username' and 'sub' are not present
const userName3 = getNameFromUserData(
{ ...userProfile, preferred_username: '', sub: '' },
['preferred_username', 'sub', 'email']
);
expect(userName3).toEqual('testUser');
});
it('getNameFromUserData should handle the claim if it contains @', () => {
const userName1 = getNameFromUserData(
{ ...userProfile, preferred_username: 'test@gmail.com' },
['preferred_username', 'email', 'sub']
);
expect(userName1).toEqual('test');
const userName2 = getNameFromUserData(
{ ...userProfile, sub: 'test-1@gmail.com' },
['sub', 'preferred_username', 'email']
);
expect(userName2).toEqual('test-1');
});
});