Skip to content

Commit

Permalink
DESENG-542: Keycloak auth method and redirection fixed (#2456)
Browse files Browse the repository at this point in the history
* DESENG-542: Authentication fixes

* DESENG-542: URL Redirection fix

* Updated Changelog

* Fixing review comments

* Fixing review comment
  • Loading branch information
ratheesh-aot authored Apr 10, 2024
1 parent 02ca3c7 commit 5825b43
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 47 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

## April 09, 2024

- **Task**: MET Web - Some URLs not taking users to correct locations/timing out [DESENG-542](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-542)
- Created authentication provider to know the logged-in state before the App component is rendered.
- Optimized tenant identification from the url.
- Optimized path segments identification.

- **Task**: CSS Selector specificity [🎟️ DESENG-577](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-577)
- Replace the `!important` flag with more specific CSS selectors, specifically within dropdowns,
to ensure that the correct styles are applied, and that states we have not yet designed for
Expand Down
50 changes: 27 additions & 23 deletions met-web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useContext } from 'react';
import '@bcgov/design-tokens/css-prefixed/variables.css'; // Will be available to use in all component
import './App.scss';
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom';
import UserService from './services/userService';
import { useAppSelector, useAppDispatch } from './hooks';
import { MidScreenLoader, MobileToolbar } from './components/common';
import { Box, Container, useMediaQuery, Theme, Toolbar } from '@mui/material';
Expand All @@ -26,6 +25,8 @@ import { openNotification } from 'services/notificationService/notificationSlice
import i18n from './i18n';
import DocumentTitle from 'DocumentTitle';
import { Language } from 'constants/language';
import { AuthKeyCloakContext } from './components/auth/AuthKeycloakContext';
import { determinePathSegments, findTenantInPath } from './utils';

interface Translations {
[languageId: string]: { [key: string]: string };
Expand All @@ -36,17 +37,13 @@ const App = () => {
const isMediumScreen: boolean = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'));
const dispatch = useAppDispatch();
const roles = useAppSelector((state) => state.user.roles);
const isLoggedIn = useAppSelector((state) => state.user.authentication.authenticated);
const authenticationLoading = useAppSelector((state) => state.user.authentication.loading);
const pathSegments = window.location.pathname.split('/');
const pathSegments = determinePathSegments();
const language: LanguageState = useAppSelector((state) => state.language);
const basename = pathSegments[1].toLowerCase();
const basename = findTenantInPath();
const tenant: TenantState = useAppSelector((state) => state.tenant);
const [translations, setTranslations] = useState<Translations>({});

useEffect(() => {
UserService.initKeycloak(dispatch);
}, [dispatch]);
const { isAuthenticated } = useContext(AuthKeyCloakContext);

useEffect(() => {
sessionStorage.setItem('apiurl', String(AppConfig.apiUrl));
Expand Down Expand Up @@ -83,28 +80,35 @@ const App = () => {
};

const loadTenant = () => {
// Load default tenant if in a single tenant environment
if (AppConfig.tenant.isSingleTenantEnvironment) {
fetchTenant(AppConfig.tenant.defaultTenant);
return;
}

const defaultTenant = AppConfig.tenant.defaultTenant;
const defaultLanguage = AppConfig.language.defaultLanguageId;

// Determine the appropriate URL to redirect
const redirectToDefaultUrl = (base: string, includeLanguage = true) => {
const languageSegment = includeLanguage ? `/${defaultLanguage}` : '/home';
window.location.replace(`/${base}${languageSegment}`);
};

const shouldIncludeLanguage = !isAuthenticated;

if (basename) {
fetchTenant(basename);
if (pathSegments.length === 2) {
const defaultLanguage = AppConfig.language.defaultLanguageId; // Set the default language here
const defaultUrl = `/${basename}/${defaultLanguage}`;
window.location.replace(defaultUrl);
// if language or admin dashboard url not set
if (pathSegments.length < 2) {
redirectToDefaultUrl(basename, shouldIncludeLanguage);
}
return;
}

if (!basename && AppConfig.tenant.defaultTenant) {
const defaultLanguage = AppConfig.language.defaultLanguageId; // Set the default language here
const defaultUrl = `/${AppConfig.tenant.defaultTenant}/${defaultLanguage}`;
window.location.replace(defaultUrl);
} else if (defaultTenant) {
fetchTenant(defaultTenant);
redirectToDefaultUrl(defaultTenant, shouldIncludeLanguage);
} else {
dispatch(loadingTenant(false));
}

dispatch(loadingTenant(false));
};

const preloadTranslations = async () => {
Expand Down Expand Up @@ -185,7 +189,7 @@ const App = () => {
);
}

if (!isLoggedIn) {
if (!isAuthenticated) {
return (
<Router basename={tenant.basename}>
<DocumentTitle />
Expand Down
57 changes: 57 additions & 0 deletions met-web/src/components/auth/AuthKeycloakContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { createContext, useState, useEffect } from 'react';
import { useAppDispatch } from 'hooks';
import UserService from '../../services/userService';
import { _kc } from 'constants/tenantConstants';
const KeycloakData = _kc;

export interface AuthKeyCloakContextProps {
isAuthenticated: boolean;
isAuthenticating: boolean; // Add a flag to indicate ongoing authentication process
keycloakInstance: Keycloak.default | null;
}

export const AuthKeyCloakContext = createContext<AuthKeyCloakContextProps>({
isAuthenticated: false,
isAuthenticating: true, // Initially, authentication is in progress
keycloakInstance: null, // Initial value
});

export const AuthKeyCloakContextProvider = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
const dispatch = useAppDispatch();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(true); // State to manage authentication loading status

useEffect(() => {
const initAuth = async () => {
try {
const authenticated = await KeycloakData.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
checkLoginIframe: false,
});
setIsAuthenticated(authenticated); // Update authentication state
UserService.setKeycloakInstance(KeycloakData);
UserService.setAuthData(dispatch);
} catch (error) {
console.error('Authentication initialization failed:', error);
} finally {
setIsAuthenticating(false); // Indicate that authentication process is complete
}
};
initAuth();
}, [dispatch]);

// Render children only after the authentication status is determined
return (
<AuthKeyCloakContext.Provider
value={{
isAuthenticated,
isAuthenticating,
keycloakInstance: KeycloakData,
}}
>
{!isAuthenticating && children}
</AuthKeyCloakContext.Provider>
);
};
6 changes: 5 additions & 1 deletion met-web/src/components/layout/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ const Footer = () => {
<MetLabel>{translate('footer.moreInfo')}</MetLabel>
</Grid>
<Grid item xs={6}>
<Link to={`/${LanguageId}`} color={Palette.text.primary} component={NavLink}>
<Link
to={isLoggedIn ? `/home` : `/${LanguageId}`}
color={Palette.text.primary}
component={NavLink}
>
{translate('footer.home')}
</Link>
</Grid>
Expand Down
8 changes: 4 additions & 4 deletions met-web/src/components/layout/Header/InternalHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const InternalHeader = ({ drawerWidth = 280 }: HeaderProps) => {
cursor: 'pointer',
}}
onClick={() => {
navigate('/');
navigate('/home');
}}
onError={(_e) => {
setImageError(true);
Expand All @@ -87,15 +87,15 @@ const InternalHeader = ({ drawerWidth = 280 }: HeaderProps) => {
marginRight: { xs: '1em', md: '3em' },
}}
onClick={() => {
navigate('/');
navigate('/home');
}}
alt="British Columbia Logo"
/>
</When>
{isMediumScreen ? (
<HeaderTitle
onClick={() => {
navigate('/');
navigate('/home');
}}
sx={{ flexGrow: 1, cursor: 'pointer' }}
>
Expand All @@ -104,7 +104,7 @@ const InternalHeader = ({ drawerWidth = 280 }: HeaderProps) => {
) : (
<HeaderTitle
onClick={() => {
navigate('/');
navigate('/home');
}}
sx={{ flexGrow: 1, cursor: 'pointer' }}
>
Expand Down
2 changes: 1 addition & 1 deletion met-web/src/components/layout/SideNav/SideNavElements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface Route {
}

export const Routes: Route[] = [
{ name: 'Home', path: '/', base: '/', authenticated: false, allowedRoles: [] },
{ name: 'Home', path: '/home', base: '/', authenticated: false, allowedRoles: [] },
{
name: 'Engagements',
path: '/engagements',
Expand Down
2 changes: 1 addition & 1 deletion met-web/src/constants/tenantConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ export const tenantDetail: ITenantDetail = {
};

// eslint-disable-next-line
export const _kc: Keycloak.KeycloakInstance = new (Keycloak as any)(tenantDetail);
export const _kc: Keycloak.default = new (Keycloak as any)(tenantDetail);
15 changes: 10 additions & 5 deletions met-web/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Formio } from '@formio/react';
import MetFormioComponents from 'met-formio';
import '@bcgov/bc-sans/css/BCSans.css';
import { HelmetProvider } from 'react-helmet-async';
import { AuthKeyCloakContextProvider } from 'components/auth/AuthKeycloakContext';

Formio.use(MetFormioComponents);
Formio.Utils.Evaluator.noeval = false;
Expand All @@ -17,15 +18,19 @@ Formio.Utils.Evaluator.noeval = false;
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
// <React.StrictMode>

<HelmetProvider>
<Provider store={store}>
<ThemeProvider theme={BaseTheme}>
<StyledEngineProvider injectFirst>
<App />
</StyledEngineProvider>
</ThemeProvider>
<AuthKeyCloakContextProvider>
<ThemeProvider theme={BaseTheme}>
<StyledEngineProvider injectFirst>
<App />
</StyledEngineProvider>
</ThemeProvider>
</AuthKeyCloakContextProvider>
</Provider>
</HelmetProvider>,

// </React.StrictMode>
);

Expand Down
2 changes: 1 addition & 1 deletion met-web/src/routes/AuthenticatedRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const AuthenticatedRoutes = () => {
<ScrollToTop />
<FormioListener />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/home" element={<Dashboard />} />
<Route path="/engagements" element={<EngagementListing />} />
<Route path="/surveys" element={<SurveyListing />} />
<Route path="/surveys/create" element={<CreateSurvey />} />
Expand Down
24 changes: 13 additions & 11 deletions met-web/src/services/userService/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { _kc } from 'constants/tenantConstants';
import {
userToken,
userDetails,
Expand All @@ -17,18 +16,20 @@ import { getMembershipsByUser } from 'services/membershipService';
import { USER_ROLES } from 'services/userService/constants';
import { getBaseUrl } from 'helper';

const KeycloakData = _kc;
let KeycloakData: Keycloak.default;

/**
* Initializes Keycloak instance.
* Setting Keycloak instance.
*/
const initKeycloak = async (dispatch: Dispatch<AnyAction>) => {
const setKeycloakInstance = (instance: Keycloak.default) => {
KeycloakData = instance;
};
/**
* Setting user authentication data in storage
*/
const setAuthData = async (dispatch: Dispatch<AnyAction>) => {
try {
const authenticated = await KeycloakData.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
checkLoginIframe: false,
});
const authenticated = !!KeycloakData.token;
if (!authenticated) {
console.warn('not authenticated!');
dispatch(userAuthentication(authenticated));
Expand Down Expand Up @@ -151,14 +152,15 @@ const getAssignedEngagements = async (externalId: string, roles: string[]) => {
};

const UserService = {
initKeycloak,
setAuthData,
updateUser,
doLogin,
doLogout,
isLoggedIn,
getToken,
hasRole,
hasAdminRole,
setKeycloakInstance,
};

export default UserService;
13 changes: 13 additions & 0 deletions met-web/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,16 @@ export const blobToFile = (theBlob: Blob, fileName: string): File => {
//Cast to a File() type
return <File>theBlob;
};

export const determinePathSegments = () => {
const url = new URL(window.location.href);
// filters out empty segments, which can occur if there are leading or trailing slashes in the pathname
const pathSegments = url.pathname.split('/').filter((segment) => segment.trim() !== '');
return pathSegments;
};

export const findTenantInPath = () => {
// finding tenant from the path segments
const pathSegments = determinePathSegments();
return pathSegments.length > 0 ? pathSegments[0].toLowerCase() : '';
};

0 comments on commit 5825b43

Please sign in to comment.