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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,10 +18,7 @@ import {
PopupRequest,
PublicClientApplication,
} from '@azure/msal-browser';
import {
JWT_PRINCIPAL_CLAIMS,
UserProfile,
} from 'components/authentication/auth-provider/AuthProvider.interface';
import { UserProfile } from 'components/authentication/auth-provider/AuthProvider.interface';
import jwtDecode, { JwtPayload } from 'jwt-decode';
import { first, isNil } from 'lodash';
import { WebStorageStateStore } from 'oidc-client';
@ -212,18 +209,32 @@ export const getNameFromUserData = (
user: UserProfile,
jwtPrincipalClaims: AuthenticationConfiguration['jwtPrincipalClaims'] = []
) => {
// get the first claim from claim list
const firstClaim = first(jwtPrincipalClaims);
const nameFromEmail = getNameFromEmail(user.email);
// filter and extract the present claims in user profile
const jwtClaims = jwtPrincipalClaims.reduce(
(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
* for fallback if preferred_username is not present then return the name from email
*/
if (firstClaim === JWT_PRINCIPAL_CLAIMS.PREFERRED_USERNAME) {
return user.preferred_username ?? nameFromEmail;
// get the first claim from claims list
const firstClaim = first(jwtClaims);
let userName = '';
// 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) => {

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');
});
});