diff --git a/package.json b/package.json
index f8548024..25cd2195 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "stromae",
- "version": "2.4.12",
+ "version": "2.4.13",
"description": "Web application for the management of questionnaires powered by Lunatic",
"repository": {
"type": "git",
@@ -18,8 +18,7 @@
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"date-fns": "^2.29.3",
- "jwt-decode": "^3.1.2",
- "oidc-client-ts": "^2.3.0",
+ "oidc-spa": "^2.0.3",
"powerhooks": "^1.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/src/components/auth/hoc/hoc.js b/src/components/auth/hoc/hoc.js
index 34cbf130..d014fee8 100644
--- a/src/components/auth/hoc/hoc.js
+++ b/src/components/auth/hoc/hoc.js
@@ -1,9 +1,9 @@
-import { useContext } from 'react';
-import { AuthContext } from '../provider/component';
+import { useAuth } from 'utils/hooks/useAuth';
const secure = (WrappedComponent) => {
const Component = (props) => {
- const { isUserLoggedIn, login } = useContext(AuthContext);
+ const { oidc } = useAuth();
+ const { isUserLoggedIn, login } = oidc;
const { otherProps } = props;
const ReturnedComponent = ;
@@ -11,7 +11,9 @@ const secure = (WrappedComponent) => {
if (isUserLoggedIn) {
return ReturnedComponent;
}
- login();
+ login({
+ doesCurrentHrefRequiresAuth: true,
+ });
return null;
};
diff --git a/src/components/auth/provider/component.js b/src/components/auth/provider/component.js
index f40b70e3..67e192e3 100644
--- a/src/components/auth/provider/component.js
+++ b/src/components/auth/provider/component.js
@@ -1,56 +1,60 @@
-import React, { useEffect, useState } from 'react';
-import { errorDictionary } from '../../../i18n';
-import { createOidcClient } from '../../../utils/auth';
-import { NONE, OIDC } from '../../../utils/constants';
-import { listenActivity } from '../../../utils/events';
+import { LoaderSimple } from 'components/shared/loader';
+import { createOidcProvider } from 'oidc-spa/react';
+import React from 'react';
+import { OIDC, READ_ONLY } from '../../../utils/constants';
import { environment, oidcConf } from '../../../utils/read-env-vars';
-import { LoaderSimple } from '../../shared/loader';
const dummyOidcClient = {
isUserLoggedIn: true,
- getUser: () => ({ accessToken: null, sub: '' }),
logout: () => (window.location.href = '/'),
+ getTokens: () => ({
+ accessToken: null,
+ idToken: null,
+ refreshToken: null,
+ refreshTokenExpirationTime: null,
+ accessTokenExpirationTime: null,
+ }),
renewToken: () => {},
};
const { AUTH_TYPE, IDENTITY_PROVIDER, PORTAIL_URL } = environment;
+const { authUrl, realm, client_id } = oidcConf;
+
+function getCurrentSurvey(path) {
+ const temp = path.split('/questionnaire/');
+ if (temp.length > 1) {
+ const idQ = temp[1].slice(0, temp[1].indexOf('/'));
+ return idQ.substr(0, idQ.indexOf('2')).toLowerCase();
+ }
+ return '';
+}
+
+export const getLogoutUrl = () =>
+ `${PORTAIL_URL}/${getCurrentSurvey(window.location.href)}`;
+
+const isReadOnlyMode = window.location.pathname.startsWith(`/${READ_ONLY}`);
+
+const getExtraQueryParams = () => {
+ if (isReadOnlyMode) return { kc_idp_hint: IDENTITY_PROVIDER };
+ return {};
+};
export const AuthContext = React.createContext();
export function AuthProvider({ children }) {
- const [oidcClient, setOidcClient] = useState(() => {
- switch (AUTH_TYPE) {
- case OIDC:
- return null;
- case NONE:
- return dummyOidcClient;
- default:
- throw new Error(errorDictionary.noAuthFile);
- }
- });
-
- useEffect(() => {
- if (AUTH_TYPE !== OIDC) {
- return;
- }
-
- (async () => {
- const oidcClient = await createOidcClient({
- url: oidcConf.authUrl,
- realm: oidcConf.realm,
- clientId: oidcConf.client_id,
- identityProvider: IDENTITY_PROVIDER,
- urlPortail: PORTAIL_URL,
- evtUserActivity: listenActivity,
- });
-
- setOidcClient(oidcClient);
- })();
- }, []);
-
- if (oidcClient === null) return ;
+ if (AUTH_TYPE === OIDC) {
+ const { OidcProvider } = createOidcProvider({
+ issuerUri: `${authUrl}/realms/${realm}`,
+ clientId: client_id,
+ getExtraQueryParams: getExtraQueryParams,
+ // See above for other parameters
+ });
+ return }>{children};
+ }
return (
- {children}
+
+ {children}
+
);
}
diff --git a/src/components/navigation/burgerMenu/burgerMenu.js b/src/components/navigation/burgerMenu/burgerMenu.js
index 13b14913..e57dda9b 100644
--- a/src/components/navigation/burgerMenu/burgerMenu.js
+++ b/src/components/navigation/burgerMenu/burgerMenu.js
@@ -4,11 +4,10 @@ import Close from '@material-ui/icons/Close';
import ExitToApp from '@material-ui/icons/ExitToApp';
import Help from '@material-ui/icons/Help';
import MenuIcon from '@material-ui/icons/Menu';
-import { useContext, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { burgerDictionary } from '../../../i18n';
import { HOUSEHOLD } from '../../../utils/constants';
import { SIMPLE_CLICK_EVENT, paradataHandler } from '../../../utils/events';
-import { AuthContext } from '../../auth/provider/component';
import { AppVersion } from '../../designSystem/AppVersion';
import { AssistanceConfirm } from '../../modals/assistance';
import './burgerMenu.css';
@@ -22,8 +21,6 @@ const BurgerMenu = ({ metadata, currentPage, logoutAndClose }) => {
const [assistance, setAssistance] = useState(false);
const { inseeContext } = metadata;
- const { isUserLoggedIn } = useContext(AuthContext);
-
useEffect(() => {
window.addEventListener('scroll', closeMenu);
return () => {
@@ -53,7 +50,7 @@ const BurgerMenu = ({ metadata, currentPage, logoutAndClose }) => {
{burgerDictionary.help}
- {isUserLoggedIn && inseeContext === HOUSEHOLD && (
+ {inseeContext === HOUSEHOLD && (
{
const { putData, putStateData, postParadata } = useAPI(idSU, idQ);
- const { logout, getUser, isUserLoggedIn } = useContext(AuthContext);
+ const {
+ oidc: { logout, isUserLoggedIn },
+ } = useAuth();
+
+ const { user } = useUser();
const { getReferentiel } = useGetReferentiel();
@@ -74,12 +79,12 @@ const OrchestratorManager = () => {
});
const logoutAndClose = useConstCallback(() => {
- logout('portail');
+ logout({ redirectTo: 'specific url', url: getLogoutUrl() });
});
useEffect(() => {
if (isUserLoggedIn && questionnaire) {
- LOGGER.addMetadata({ idSession: getUser().sub });
+ LOGGER.addMetadata({ idSession: user.sub });
LOGGER.log(INIT_SESSION_EVENT);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/src/utils/auth/index.js b/src/utils/auth/index.js
deleted file mode 100644
index ad5f17ac..00000000
--- a/src/utils/auth/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { createOidcClient } from './oidcClient';
diff --git a/src/utils/auth/oidcClient.js b/src/utils/auth/oidcClient.js
deleted file mode 100644
index 7641efaa..00000000
--- a/src/utils/auth/oidcClient.js
+++ /dev/null
@@ -1,262 +0,0 @@
-/* eslint-disable no-labels, no-lone-blocks */
-import jwt_decode from 'jwt-decode';
-import { UserManager } from 'oidc-client-ts';
-import {
- addParamToUrl,
- retrieveParamFromUrl,
-} from 'powerhooks/tools/urlSearchParams';
-import { READ_ONLY } from '../constants';
-import { Deferred } from '../tools/Deferred';
-import { fnv1aHashToHex } from '../tools/fnv1aHashToHex';
-
-export const createOidcClient = async ({
- url,
- realm,
- clientId,
- identityProvider,
- evtUserActivity,
- urlPortail,
-}) => {
- const isReadOnlyMode = window.location.pathname.startsWith(`/${READ_ONLY}`);
- const configHash = fnv1aHashToHex(`${url} ${realm} ${clientId}`);
- const configHashKey = 'configHash';
-
- const userManager = new UserManager({
- authority: `${url}/realms/${realm}`,
- client_id: clientId,
- redirect_uri: '' /* provided when calling login */,
- response_type: 'code',
- scope: 'openid profile',
- automaticSilentRenew: false,
- silent_redirect_uri: `${window.location.origin}/silent-sso.html?${configHashKey}=${configHash}`,
- });
-
- const login = async () => {
- const { newUrl: redirect_uri } = addParamToUrl({
- url: window.location.href,
- name: configHashKey,
- value: configHash,
- });
-
- await userManager.signinRedirect({
- redirect_uri,
- extraQueryParams: isReadOnlyMode
- ? { kc_idp_hint: identityProvider }
- : null,
- redirectMethod: 'replace',
- });
- return new Promise(() => {});
- };
-
- read_successful_login_query_params: {
- let url = window.location.href;
- {
- const result = retrieveParamFromUrl({ name: configHashKey, url });
-
- if (!result.wasPresent || result.value !== configHash) {
- break read_successful_login_query_params;
- }
-
- url = result.newUrl;
- }
-
- {
- const result = retrieveParamFromUrl({ name: 'error', url });
-
- if (result.wasPresent) {
- throw new Error(`OIDC error: ${result.value}`);
- }
- }
-
- // signinRedirectCallback required url in params
- let loginSuccessUrl = 'https://dummy.com';
-
- for (const name of ['code', 'state', 'session_state']) {
- const result = retrieveParamFromUrl({ name, url });
-
- loginSuccessUrl = addParamToUrl({
- url: loginSuccessUrl,
- name: name,
- value: result.value,
- }).newUrl;
- url = result.newUrl;
- }
-
- try {
- await userManager.signinRedirectCallback(loginSuccessUrl);
- } catch {
- //NOTE: The user has likely pressed the back button just after logging in.
- }
-
- window.history.pushState(null, '', url);
- }
-
- async function silentSignInGetAccessToken() {
- const dLoginSuccessUrl = new Deferred();
-
- const timeout = setTimeout(
- () =>
- dLoginSuccessUrl.reject(
- new Error(`SSO silent login timeout with clientId: ${clientId}`)
- ),
- 5000
- );
-
- const listener = (event) => {
- if (
- event.origin !== window.location.origin ||
- typeof event.data !== 'string'
- ) {
- return;
- }
-
- const url = event.data;
-
- {
- let result;
-
- try {
- result = retrieveParamFromUrl({ name: configHashKey, url });
- } catch {
- // This could possibly happen if url is not a valid url.
- return;
- }
-
- if (!result.wasPresent || result.value !== configHash) {
- return;
- }
- }
-
- clearTimeout(timeout);
-
- window.removeEventListener('message', listener);
-
- {
- const result = retrieveParamFromUrl({ name: 'error', url });
-
- if (result.wasPresent) {
- dLoginSuccessUrl.resolve(undefined);
- return;
- }
- }
-
- let loginSuccessUrl = 'https://dummy.com';
-
- for (const name of ['code', 'state', 'session_state']) {
- const result = retrieveParamFromUrl({ name, url });
-
- loginSuccessUrl = addParamToUrl({
- url: loginSuccessUrl,
- name: name,
- value: result.value,
- }).newUrl;
- }
-
- dLoginSuccessUrl.resolve(loginSuccessUrl);
- };
-
- window.addEventListener('message', listener, false);
-
- userManager.signinSilent({ silentRequestTimeoutInSeconds: 1 }).catch(() => {
- /* error expected */
- });
-
- const loginSuccessUrl = await dLoginSuccessUrl.pr;
-
- if (loginSuccessUrl === undefined) {
- return undefined;
- }
-
- const user = await userManager.signinRedirectCallback(loginSuccessUrl);
-
- return user;
- }
-
- let currentUser = await userManager.getUser();
-
- let currentAccessToken = currentUser?.access_token ?? '';
-
- if (currentUser === null) {
- const user = await silentSignInGetAccessToken();
-
- if (user) {
- currentUser = user;
- }
- }
-
- if (currentUser === null) {
- return {
- isUserLoggedIn: false,
- login,
- };
- }
- const oidcClient = {
- isUserLoggedIn: true,
- getUser: () => ({
- accessToken: currentUser.access_token,
- sub: currentUser.sub,
- }),
- logout: async ({ redirectTo }) => {
- await userManager.signoutRedirect({
- post_logout_redirect_uri: (() => {
- switch (redirectTo) {
- case 'portail':
- return `${urlPortail}/${getCurrentSurvey(window.location.href)}`;
- case 'home':
- return window.location.origin;
- default:
- return urlPortail;
- }
- })(),
- });
- return new Promise(() => {});
- },
- renewToken: async () => {
- const user = await userManager.signinSilent();
- currentUser = user;
- currentAccessToken = user.access_token;
- },
- };
-
- (function callee() {
- const msBeforeExpiration =
- getAccessTokenExpirationTime(currentAccessToken) - Date.now();
-
- setTimeout(async () => {
- console.log(
- `OIDC access token will expire in ${minValiditySecond} seconds, waiting for user activity before renewing`
- );
-
- await evtUserActivity();
-
- console.log('User activity detected. Refreshing access token now');
-
- try {
- await oidcClient.renewToken();
- } catch {
- console.log("Can't refresh OIDC access token, getting a new one");
- //NOTE: Never resolves
- await login();
- }
-
- callee();
- }, msBeforeExpiration - minValiditySecond * 1000);
- })();
-
- return oidcClient;
-};
-
-const minValiditySecond = 25;
-
-function getAccessTokenExpirationTime(accessToken) {
- return jwt_decode(accessToken).exp * 1000;
-}
-
-function getCurrentSurvey(path) {
- const temp = path.split('/questionnaire/');
- if (temp.length > 1) {
- const idQ = temp[1].slice(0, temp[1].indexOf('/'));
- return idQ.substr(0, idQ.indexOf('2')).toLowerCase();
- }
- return '';
-}
diff --git a/src/utils/hooks/api.js b/src/utils/hooks/api.js
index a886dc5f..b911e88f 100644
--- a/src/utils/hooks/api.js
+++ b/src/utils/hooks/api.js
@@ -1,10 +1,10 @@
-import { useContext, useEffect, useState } from 'react';
-import { AuthContext } from '../../components/auth/provider/component';
+import { useEffect, useState } from 'react';
import { errorDictionary } from '../../i18n';
import { API } from '../api';
import { getFetcherForLunatic } from '../api/fetcher';
import { DEFAULT_DATA_URL, DEFAULT_METADATA_URL } from '../constants';
import { environment } from '../read-env-vars';
+import { useAuth } from './useAuth';
import { useConstCallback } from './useConstCallback';
const { API_URL: apiUrl } = environment;
@@ -19,17 +19,17 @@ const getErrorMessage = (response, type = 'q') => {
};
export const useGetReferentiel = (nomenclatures) => {
- const oidcClient = useContext(AuthContext);
+ const { oidc } = useAuth();
const getReferentiel = useConstCallback((refName) => {
const finalUrl = `${apiUrl}/api/nomenclature/${refName}`;
- return getFetcherForLunatic(oidcClient.getUser().accessToken)(finalUrl);
+ return getFetcherForLunatic(oidc.getTokens().accessToken)(finalUrl);
});
const getReferentielForVizu = useConstCallback((refName) => {
if (nomenclatures && Object.keys(nomenclatures).includes(refName)) {
const finalUrl = nomenclatures[refName];
- return getFetcherForLunatic(oidcClient.getUser().accessToken)(finalUrl);
+ return getFetcherForLunatic(oidc.getTokens().accessToken)(finalUrl);
}
// No nomenclature, return empty array to lunatic
return Promise.resolve([]);
@@ -39,48 +39,44 @@ export const useGetReferentiel = (nomenclatures) => {
};
export const useAPI = (surveyUnitID, questionnaireID) => {
- const oidcClient = useContext(AuthContext);
+ const { oidc } = useAuth();
const getRequiredNomenclatures = useConstCallback(() =>
API.getRequiredNomenclatures(apiUrl)(questionnaireID)(
- oidcClient.getUser().accessToken
+ oidc.getTokens().accessToken
)
);
const getQuestionnaire = useConstCallback(() =>
- API.getQuestionnaire(apiUrl)(questionnaireID)(
- oidcClient.getUser().accessToken
- )
+ API.getQuestionnaire(apiUrl)(questionnaireID)(oidc.getTokens().accessToken)
);
const getMetadata = useConstCallback(() =>
- API.getMetadata(apiUrl)(questionnaireID)(oidcClient.getUser().accessToken)
+ API.getMetadata(apiUrl)(questionnaireID)(oidc.getTokens().accessToken)
);
const getSuData = useConstCallback(() =>
- API.getSuData(apiUrl)(surveyUnitID)(oidcClient.getUser().accessToken)
+ API.getSuData(apiUrl)(surveyUnitID)(oidc.getTokens().accessToken)
);
const getPDF = useConstCallback(() =>
- API.getDepositProof(apiUrl)(surveyUnitID)(oidcClient.getUser().accessToken)
+ API.getDepositProof(apiUrl)(surveyUnitID)(oidc.getTokens().accessToken)
);
const putSuData = useConstCallback((body) =>
- API.putSuData(apiUrl)(surveyUnitID)(oidcClient.getUser().accessToken)(body)
+ API.putSuData(apiUrl)(surveyUnitID)(oidc.getTokens().accessToken)(body)
);
const putData = useConstCallback((body) =>
- API.putData(apiUrl)(surveyUnitID)(oidcClient.getUser().accessToken)(body)
+ API.putData(apiUrl)(surveyUnitID)(oidc.getTokens().accessToken)(body)
);
const putStateData = useConstCallback((body) =>
- API.putStateData(apiUrl)(surveyUnitID)(oidcClient.getUser().accessToken)(
- body
- )
+ API.putStateData(apiUrl)(surveyUnitID)(oidc.getTokens().accessToken)(body)
);
const postParadata = useConstCallback((body) =>
- API.postParadata(apiUrl)(oidcClient.getUser().accessToken)(body)
+ API.postParadata(apiUrl)(oidc.getTokens().accessToken)(body)
);
return {
diff --git a/src/utils/hooks/useAuth.js b/src/utils/hooks/useAuth.js
new file mode 100644
index 00000000..c4c14a98
--- /dev/null
+++ b/src/utils/hooks/useAuth.js
@@ -0,0 +1,35 @@
+/* eslint-disable react-hooks/rules-of-hooks */
+import { AuthContext } from 'components/auth/provider/component';
+import { decodeJwt } from 'oidc-spa';
+import { useOidc } from 'oidc-spa/react';
+import { useContext, useMemo } from 'react';
+import { OIDC } from 'utils/constants';
+import { environment } from 'utils/read-env-vars';
+
+const { AUTH_TYPE } = environment;
+
+export const useAuth = () => {
+ if (AUTH_TYPE === OIDC) {
+ return useOidc();
+ } else {
+ const dummyClient = useContext(AuthContext);
+ return { oidc: dummyClient };
+ }
+};
+
+export const useUser = () => {
+ const { oidc } = useAuth();
+
+ if (!oidc.isUserLoggedIn) {
+ throw new Error('This hook should be used only on authenticated routes');
+ }
+
+ const { idToken } = oidc.getTokens();
+
+ const user = useMemo(() => {
+ if (AUTH_TYPE === OIDC) return decodeJwt(idToken);
+ return { preferred_username: null, sub: '' };
+ }, [idToken]);
+
+ return { user };
+};
diff --git a/yarn.lock b/yarn.lock
index 17de03b2..9172990e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4629,9 +4629,9 @@ crypto-browserify@^3.11.0:
randomfill "^1.0.3"
crypto-js@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
- integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
+ integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
crypto-random-string@^1.0.0:
version "1.0.0"
@@ -10121,6 +10121,15 @@ oidc-client-ts@^2.3.0:
crypto-js "^4.1.1"
jwt-decode "^3.1.2"
+oidc-spa@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/oidc-spa/-/oidc-spa-2.0.3.tgz#387a6c92b2fbd097d75ca9f3fa4b10f7f5213cdc"
+ integrity sha512-pZJgjpdVr2CRGKhqQGj6z7qu1INo9/xP6HkkqIata3zXEmFrIK/zv6SCpDdFMfeIVfqQWV946FVbccUqBrUlcA==
+ dependencies:
+ jwt-decode "^3.1.2"
+ oidc-client-ts "^2.3.0"
+ tsafe "^1.6.5"
+
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"