Skip to content

Commit

Permalink
add oidc-spa (#109)
Browse files Browse the repository at this point in the history
* wip add oidc-spa

* rework App.jsx and add configuration & oidc in index.js

* change package.json

* remove unused code

* modify queryClient staleTime value

* change configuration.json
  • Loading branch information
RenauxLeaInsee authored Jun 5, 2024
1 parent 48b5991 commit 973b85c
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 2,577 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "sonor",
"version": "0.5.33",
"version": "0.5.34",
"private": true,
"dependencies": {
"@tanstack/react-query": "4.0.5",
"@testing-library/jest-dom": "^4.2.4",
"async-wait-until": "^2.0.5",
"awesome-debounce-promise": "^2.1.0",
Expand All @@ -12,8 +13,8 @@
"cypress-multi-reporters": "^1.2.4",
"font-awesome": "^4.7.0",
"history": "^5.0.0",
"keycloak-js": "4.0.0-beta.2",
"mocha-junit-reporter": "^1.23.3",
"oidc-spa": "4.5.1",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-bootstrap": "^1.0.1",
Expand Down
5 changes: 3 additions & 2 deletions public/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
"PEARL_JAM_URL": "http://localhost:7777",
"QUEEN_URL_BACK_END": "http://localhost:7777",
"QUEEN_URL_FRONT_END": "http://localhost:7777",

"ISSUER_URI": "http://localhost:7777",
"OIDC_CLIENT_ID": "clientId",
"AUTHENTICATION_MODE": "anonymous",
"_AUTHENTICATION_MODE_COMMENT_": "Use 'keycloak' or 'anonymous'"
"_AUTHENTICATION_MODE_COMMENT_": "Use 'oidc' or 'anonymous'"
}

7 changes: 7 additions & 0 deletions public/silent-sso.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
51 changes: 51 additions & 0 deletions src/Authentication/useAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect } from "react";
import { createReactOidc } from "oidc-spa/react";

/**
* By default, without initialization we use a mock as a return of "useOidc"
*
* This object will be used for testing purpose to simulate authentication status
*/
const mockOidc = { login: () => {}, isUserLoggedIn: false, oidcTokens: {} }
// Global method that will be replaced when oidc is initialized
let useOidc = ({assertUserLoggedIn}) => mockOidc;

/**
* Helper method used for tests, set a fake Oidc authentication state
*/
export const mockOidcForUser = () => {
window.localStorage.setItem("AUTHENTICATION_MODE", "oidc")
mockOidc.isUserLoggedIn = true
mockOidc.oidcTokens = {accessToken: '12031203'}
}
export const mockOidcFailed = () => {
mockOidc.isUserLoggedIn = false
mockOidc.oidcTokens = {}
}

/**
* Initialize oidc
*/
export function initializeOidc (config) {
const oidc = createReactOidc(config)
useOidc = oidc.useOidc
return oidc;
}

/**
* Retrieve authentication status based of Oidc
*/
export function useIsAuthenticated() {
const { login, isUserLoggedIn, oidcTokens } = useOidc({ assertUserLoggedIn: false });

useEffect(() => {
if (!login) {
return;
}
login({
doesCurrentHrefRequiresAuth: true,
});
}, [login]);

return { isAuthenticated: isUserLoggedIn, tokens: oidcTokens };
}
110 changes: 39 additions & 71 deletions src/components/App/App.jsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,55 @@
import React from 'react';
import Keycloak from 'keycloak-js';
import View from '../View/View';
import DataFormatter from '../../utils/DataFormatter';
import { KEYCLOAK, ANONYMOUS } from '../../utils/constants.json';
import initConfiguration from '../../initConfiguration';
import D from '../../i18n';
import React, { useEffect, useState } from "react";
import { useIsAuthenticated } from "../../Authentication/useAuth";
import D from "../../i18n";
import View from "../View/View";
import DataFormatter from "../../utils/DataFormatter";
import { OIDC, ANONYMOUS } from "../../utils/constants.json";

class App extends React.Component {
constructor(props) {
super(props);
this.state = {
keycloak: null,
authenticated: false,
contactFailed: false,
initialisationFailed: false,
data: null,
};
}
export const App = () => {
const [authenticated, setAuthenticated] = useState(false);
const [contactFailed, setContactFailed] = useState(false);
const [data, setData] = useState(null);

async componentDidMount() {
try {
await initConfiguration();
} catch (e) {
this.setState({ initialisationFailed: true });
}
if (window.localStorage.getItem('AUTHENTICATION_MODE') === ANONYMOUS) {
const { tokens } = useIsAuthenticated();

useEffect(() => {
if (window.localStorage.getItem("AUTHENTICATION_MODE") === ANONYMOUS) {
const dataRetreiver = new DataFormatter();
dataRetreiver.getUserInfo((data) => {
if (data.error) {
this.setState({ contactFailed: true });
setContactFailed(true);
} else {
this.setState({ authenticated: true, data });
setAuthenticated(true);
setData(data);
}
});
} else if (window.localStorage.getItem('AUTHENTICATION_MODE') === KEYCLOAK) {
const keycloak = Keycloak('/keycloak.json');
keycloak.init({ onLoad: 'login-required', checkLoginIframe: false }).then((authenticated) => {
const dataRetreiver = new DataFormatter(keycloak);
dataRetreiver.getUserInfo((data) => {
this.setState({ keycloak, authenticated, data });
});
// Update 20 seconds before expiracy
const updateInterval = (keycloak.tokenParsed.exp + keycloak.timeSkew)
* 1000
- new Date().getTime()
- 20000;
setInterval(() => {
keycloak.updateToken(100).error(() => {
throw new Error('Failed to refresh token');
});
}, updateInterval);
} else if (
window.localStorage.getItem("AUTHENTICATION_MODE") === OIDC &&
tokens?.accessToken
) {
const dataRetreiver = new DataFormatter(tokens.accessToken);
dataRetreiver.getUserInfo((data) => {
setAuthenticated(data !== undefined);
setData(data);
});
}
}
}, [tokens]);

render() {
const {
keycloak, authenticated, data, contactFailed, initialisationFailed,
} = this.state;
if (keycloak || authenticated) {
if (authenticated) {
return (
<div className="App">
if (!tokens?.accessToken) {
return <div>{D.initializationFailed}</div>;
}

<View
keycloak={keycloak}
userData={data}
/>
</div>
);
}
return (<div>{D.unableToAuthenticate}</div>);
}
if (initialisationFailed) {
return (<div>{D.initializationFailed}</div>);
}
if (contactFailed) {
return (<div>{D.cannotContactServer}</div>);
}
if (authenticated && tokens?.accessToken && data) {
return (
<div>{D.initializing}</div>
<div className="App">
<View token={tokens.accessToken} userData={data} />
</div>
);
}
}

export default App;
if (contactFailed) {
return <div>{D.cannotContactServer}</div>;
}

return <div>{D.initializing}</div>;
};
69 changes: 10 additions & 59 deletions src/components/App/App.test.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// Link.react.test.js
import React from 'react';
import {
render, screen, cleanup, waitForElement,
} from '@testing-library/react';
import Keycloak from 'keycloak-js';
import { render, screen, cleanup } from "@testing-library/react";
import { NotificationManager } from 'react-notifications';
import DataFormatter from '../../utils/DataFormatter';
import App from './App';
import { App } from "./App";
import mocks from '../../tests/mocks';
import C from '../../utils/constants.json';
import { mockOidcForUser, mockOidcFailed } from "../../Authentication/useAuth";

jest.mock(
"../../../package.json",
Expand All @@ -29,7 +27,6 @@ Date.now = jest.fn(() => 1597916474000);

afterEach(cleanup);

jest.mock('keycloak-js');
jest.mock('react-notifications');
jest.mock('../../utils/DataFormatter');
jest.mock('../../initConfiguration');
Expand All @@ -53,15 +50,6 @@ const mockError = jest.fn();
NotificationManager.success = mockSuccess;
NotificationManager.error = mockError;

Keycloak.init = jest.fn(() => (Promise.resolve({ token: 'abc' })));

Keycloak.mockImplementation(() => ({
init: jest.fn(() => (Promise.resolve({ token: 'abc' }))),
updateToken: (() => ({ error: (() => {}) })),
tokenParsed: { exp: 300 },
timeSkew: 0,
}));

const updatePreferences = jest.fn((newPrefs, cb) => {
if (newPrefs.includes('simpsonkgs2020x00')) {
cb({ status: 500 });
Expand Down Expand Up @@ -104,52 +92,15 @@ DataFormatter.mockImplementation(() => ({
updatePreferences,
}));

it('Component is displayed (initializing)', async () => {
const component = render(
<App />,
);
// Should match snapshot
expect(component).toMatchSnapshot();
});

it('Component is displayed (anonymous mode)', async () => {
Object.getPrototypeOf(window.localStorage).getItem = jest.fn(() => ('anonymous'));
const component = render(
<App />,
);

await waitForElement(() => screen.getByTestId('pagination-nav'));

// Should match snapshot
it("Component is displayed ", async () => {
mockOidcForUser();
const component = render(<App />);
await screen.findByText("List of surveys");
expect(component).toMatchSnapshot();
});

it('Component is displayed (keycloak mode)', async () => {
Object.getPrototypeOf(window.localStorage).getItem = jest.fn(() => ('keycloak'));
const component = render(
<App />,
);

await waitForElement(() => screen.getByTestId('pagination-nav'));

// Should match snapshot
expect(component).toMatchSnapshot();
});

it('Could not authenticate with keycloak', async () => {
Object.getPrototypeOf(window.localStorage).getItem = jest.fn(() => ('keycloak'));

Keycloak.mockImplementation(() => ({
init: jest.fn(() => (Promise.resolve(false))),
updateToken: (() => ({ error: (() => {}) })),
tokenParsed: { exp: 300 },
timeSkew: 0,
}));

const component = render(
<App />,
);

// Should match snapshot
it("Component is not displayed ", async () => {
mockOidcFailed();
const component = render(<App />);
expect(component).toMatchSnapshot();
});
Loading

0 comments on commit 973b85c

Please sign in to comment.