diff --git a/apps/model_catalog/package-lock.json b/apps/model_catalog/package-lock.json index 916e558..772e6c1 100644 --- a/apps/model_catalog/package-lock.json +++ b/apps/model_catalog/package-lock.json @@ -20,7 +20,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "filesize": "^6.1.0", "humanparser": "^1.11.0", - "keycloak-js": "^8.0.1", + "keycloak-js": "^12.0.2", "lodash": "^4.17.20", "material-ui-chip-input": "^2.0.0-beta.2", "moment": "^2.29.1", @@ -16284,9 +16284,9 @@ "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" }, "node_modules/keycloak-js": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-8.0.2.tgz", - "integrity": "sha512-5Jy5rHx0oURTtj2T6LSMLiqyHzDzPr6ZnaY1fwCBlMsiMx7RVsIBeysYZ5yy29ojH9IMzNJzlZvuLKbEOCmgDQ==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-12.0.4.tgz", + "integrity": "sha512-O/BHtyiDrZrUnKBrVF8POojqd3gmhuiDw4FiI+FbnB14nu7G5jKFrKYZa9Q0JYKIZXHJOBzSaKQcMp2WUI+zmA==", "dependencies": { "base64-js": "1.3.1", "js-sha256": "0.9.0" @@ -40495,9 +40495,9 @@ "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" }, "keycloak-js": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-8.0.2.tgz", - "integrity": "sha512-5Jy5rHx0oURTtj2T6LSMLiqyHzDzPr6ZnaY1fwCBlMsiMx7RVsIBeysYZ5yy29ojH9IMzNJzlZvuLKbEOCmgDQ==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-12.0.4.tgz", + "integrity": "sha512-O/BHtyiDrZrUnKBrVF8POojqd3gmhuiDw4FiI+FbnB14nu7G5jKFrKYZa9Q0JYKIZXHJOBzSaKQcMp2WUI+zmA==", "requires": { "base64-js": "1.3.1", "js-sha256": "0.9.0" diff --git a/apps/model_catalog/package.json b/apps/model_catalog/package.json index 61d8a3a..fdc2b31 100644 --- a/apps/model_catalog/package.json +++ b/apps/model_catalog/package.json @@ -15,7 +15,7 @@ "eslint-plugin-react-hooks": "^4.2.0", "filesize": "^6.1.0", "humanparser": "^1.11.0", - "keycloak-js": "^8.0.1", + "keycloak-js": "^12.0.2", "lodash": "^4.17.20", "material-ui-chip-input": "^2.0.0-beta.2", "moment": "^2.29.1", diff --git a/apps/model_catalog/src/AuthWidget.js b/apps/model_catalog/src/AuthWidget.js new file mode 100644 index 0000000..892072a --- /dev/null +++ b/apps/model_catalog/src/AuthWidget.js @@ -0,0 +1,45 @@ +import React from "react"; +import Button from "@material-ui/core/Button"; +import IconButton from "@material-ui/core/IconButton"; +import PersonIcon from '@material-ui/icons/Person'; +import Tooltip from "@material-ui/core/Tooltip"; +import ContextMain from "./ContextMain"; + + +function AuthWidget(props) { + + const context = React.useContext(ContextMain); + const [auth] = context.auth; + + if (auth.authenticated || props.currentUser) { + + if (!props.currentUser) { + auth.loadUserInfo().then((userInfo) => { + console.log(userInfo); + props.setCurrentUser(userInfo.preferred_username); + }); + }; + + return ( + + + + + + ); + } else { + return ( + + ); + } +} + +export default AuthWidget; \ No newline at end of file diff --git a/apps/model_catalog/src/Introduction.js b/apps/model_catalog/src/Introduction.js index d03a9d4..981e646 100644 --- a/apps/model_catalog/src/Introduction.js +++ b/apps/model_catalog/src/Introduction.js @@ -27,10 +27,6 @@ import "./App.css"; const ResponsiveEllipsis = responsiveHOC()(LinesEllipsis); -// const openInNewTab = (url) => { -// const newWindow = window.open(url, "_blank", "noopener,noreferrer"); -// if (newWindow) newWindow.opener = null; -// }; const styles = (theme) => ({ header: { diff --git a/apps/model_catalog/src/ValidationFramework.js b/apps/model_catalog/src/ValidationFramework.js index be2f955..4a7def2 100644 --- a/apps/model_catalog/src/ValidationFramework.js +++ b/apps/model_catalog/src/ValidationFramework.js @@ -48,6 +48,7 @@ import ContextMain from "./ContextMain"; import Theme from "./theme"; import { withSnackbar } from "notistack"; import WarningBox from "./WarningBox"; +import AuthWidget from "./AuthWidget"; // if working on the appearance/layout set globals.DevMode=true // to avoid loading the models and tests over the network every time; @@ -129,6 +130,7 @@ class ValidationFramework extends React.Component { this.state = { modelData: [], testData: [], + currentUser: null, currentModel: null, currentTest: null, currentResult: null, @@ -1174,6 +1176,10 @@ class ValidationFramework extends React.Component { + this.setState({currentUser: user})} + /> diff --git a/apps/model_catalog/src/auth.js b/apps/model_catalog/src/auth.js new file mode 100644 index 0000000..c4331a1 --- /dev/null +++ b/apps/model_catalog/src/auth.js @@ -0,0 +1,114 @@ +import Keycloak from 'keycloak-js'; + + +// We start by configuring the Keycloak javascript client +// It needs to know your app id in order to authenticate users for it +const keycloak = Keycloak({ + url: 'https://iam.ebrains.eu/auth', + realm: 'hbp', + clientId: 'model-catalog', + 'public-client': true, + 'confidential-port': 0, +}); +const YOUR_APP_SCOPES = 'team email profile'; // full list at https://iam.ebrains.eu/auth/realms/hbp/.well-known/openid-configuration + + +export default function initAuth(main) { + console.log('DOM content is loaded, initialising Keycloak client...'); + keycloak + //.init({ flow: 'standard', pkceMethod: 'S256' }) + .init({ flow: 'implicit' }) + .then(() => checkAuth(main)) + .catch(console.log); +} + +function checkAuth(main) { + console.log('Keycloak client is initialised, verifying authentication...'); + + // Is the user anonymous or authenticated? + const isAuthenticated = keycloak.authenticated; + const isAnonymous = !keycloak.authenticated; + // Is this app a standalone app, a framed app or a delegate? + const isParent = (window.opener == null); + const isIframe = (window !== window.parent); + const isMainFrame = (window === window.parent); + const isStandaloneApp = isMainFrame && isParent; + const isFramedApp = isIframe && isParent; + const isDelegate = (window.opener != null); + // Posting and listening to messages + const postMessageToParentTab = (message, parentTabOrigin) => window.opener.postMessage(message, parentTabOrigin); + const listenToMessage = (callback) => window.addEventListener('message', callback); + const AUTH_MESSAGE = 'clb.authenticated'; + const myAppOrigin = window.location.origin; + // Manipulating URLs and tabs + const openTab = (url) => window.open(url); + const getCurrentURL = () => new URL(window.location); + const closeCurrentTab = () => window.close(); + + const login = (scopes) => keycloak.login({ scope: scopes }); + + // A standalone app should simply login if the user is not authenticated + // and do its business logic otherwise + if (isStandaloneApp) { + console.log('This is a standalone app...'); + if (isAnonymous) { + console.log('...which is not authenticated, starting app without authentication') + return main(keycloak); + } + if (isAuthenticated) { + console.log('...which is authenticated, starting app with authentication'); + return main(keycloak); + } + } + + // A framed app should open a delegate to do the authentication for it and listen to its messages and verify them + // If the user is authenticated, it should do its business logic + if (isFramedApp) { + console.log('This is a framed app...'); + if (isAnonymous) { + console.log('...which is not authenticated, delegating to new tab...'); + listenToMessage(verifyMessage); + return openTab(getCurrentURL()); + } + if (isAuthenticated) { + console.log('...which is authenticated, starting business logic...'); + return main(keycloak); + } + } + + // A delegate should login if the user is not authenticated + // Otherwise, it should inform its opener that the user is authenticated and close itself + if (isDelegate) { + console.log('This is a delegate tab...'); + if (isAnonymous) { + console.log('...which is not authenticated, starting login...'); + return login(YOUR_APP_SCOPES); + } + if (isAuthenticated) { + console.log('...which is authenticated, warn parent and close...'); + postMessageToParentTab(AUTH_MESSAGE, myAppOrigin); + return closeCurrentTab(); + } + } +} + +function verifyMessage(event) { + console.log('Message receveived, verifying it...'); + + const AUTH_MESSAGE = 'clb.authenticated'; + const receivedMessage = event.data; + const messageOrigin = event.origin; + const myAppOrigin = window.location.origin; + // const reload = () => window.location.reload(); // TODO: remove? + const login = (scopes) => keycloak.login({ scope: scopes }); + + + // Stop if the message is not the auth message + if (receivedMessage !== AUTH_MESSAGE) return; + + // Stop if the message is not coming from our app origin + if (messageOrigin !== myAppOrigin) return; + + // Login otherwise + return login(YOUR_APP_SCOPES); +} diff --git a/apps/model_catalog/src/index.js b/apps/model_catalog/src/index.js index 949c607..1d7bcd1 100644 --- a/apps/model_catalog/src/index.js +++ b/apps/model_catalog/src/index.js @@ -1,129 +1,12 @@ -import Keycloak from "keycloak-js"; -import React from "react"; -import ReactDOM from "react-dom"; - -import { SnackbarProvider } from "notistack"; +import React from 'react'; +import ReactDOM from 'react-dom'; +import initAuth from './auth'; import { datastore } from "./datastore"; import { ContextMainProvider } from "./ContextMain"; +import { SnackbarProvider } from "notistack"; import ValidationFramework from "./ValidationFramework"; -// We start by configuring the Keycloak javascript client -// It needs to know your app id in order to authenticate users for it -const keycloak = Keycloak({ - url: "https://iam.ebrains.eu/auth", - realm: "hbp", - clientId: "model-catalog", // TODO: change clientID to validation-framework, once client is registered -}); -const YOUR_APP_SCOPES = "team email profile"; // full list at https://iam.ebrains.eu/auth/realms/hbp/.well-known/openid-configuration - -// When ready, we initialise the keycloak client -// Once done, it will call our `checkAuth` function -window.addEventListener("DOMContentLoaded", initKeycloak); - -function initKeycloak() { - console.log("DOM content is loaded, initialising Keycloak client..."); - keycloak.init({ flow: "implicit" }).success(checkAuth).error(console.log); -} - -function checkAuth() { - console.log("Keycloak client is initialised, verifying authentication..."); - - // Is the user anonymous or authenticated? - const isAuthenticated = keycloak.authenticated; - const isAnonymous = !keycloak.authenticated; - // Is this app a standalone app, a framed app or a delegate? - const isParent = window.opener == null; - const isIframe = window !== window.parent; - const isMainFrame = window === window.parent; - const isStandaloneApp = isMainFrame && isParent; - const isFramedApp = isIframe && isParent; - const isDelegate = window.opener != null; - // Posting and listening to messages - const postMessageToParentTab = (message, parentTabOrigin) => - window.opener.postMessage(message, parentTabOrigin); - const listenToMessage = (callback) => - window.addEventListener("message", callback); - const AUTH_MESSAGE = "clb.authenticated"; - const myAppOrigin = window.location.origin; - // Manipulating URLs and tabs - const openTab = (url) => window.open(url); - const getCurrentURL = () => new URL(window.location); - const closeCurrentTab = () => window.close(); - - const login = (scopes) => keycloak.login({ scope: scopes }); - - // A standalone app should simply login if the user is not authenticated - // and do its business logic otherwise - if (isStandaloneApp) { - console.log("This is a standalone app..."); - if (isAnonymous) { - console.log("...which is not authenticated, starting login..."); - return login(YOUR_APP_SCOPES); - } - if (isAuthenticated) { - console.log( - "...which is authenticated, starting business logic..." - ); - return doBusinessLogic(keycloak); - } - } - - // A framed app should open a delegate to do the authentication for it and listen to its messages and verify them - // If the user is authenticated, it should do its business logic - if (isFramedApp) { - console.log("This is a framed app..."); - if (isAnonymous) { - console.log( - "...which is not authenticated, delegating to new tab..." - ); - listenToMessage(verifyMessage); - return openTab(getCurrentURL()); - } - if (isAuthenticated) { - console.log( - "...which is authenticated, starting business logic..." - ); - return doBusinessLogic(keycloak); - } - } - - // A delegate should login if the user is not authenticated - // Otherwise, it should inform its opener that the user is authenticated and close itself - if (isDelegate) { - console.log("This is a delegate tab..."); - if (isAnonymous) { - console.log("...which is not authenticated, starting login..."); - return login(YOUR_APP_SCOPES); - } - if (isAuthenticated) { - console.log("...which is authenticated, warn parent and close..."); - postMessageToParentTab(AUTH_MESSAGE, myAppOrigin); - return closeCurrentTab(); - } - } -} - -function verifyMessage(event) { - console.log("Message receveived, verifying it..."); - - const AUTH_MESSAGE = "clb.authenticated"; - const receivedMessage = event.data; - const messageOrigin = event.origin; - const myAppOrigin = window.location.origin; - // const reload = () => window.location.reload(); // TODO: remove? - const login = (scopes) => keycloak.login({ scope: scopes }); - - // Stop if the message is not the auth message - if (receivedMessage !== AUTH_MESSAGE) return; - - // Stop if the message is not coming from our app origin - if (messageOrigin !== myAppOrigin) return; - - // Login otherwise - return login(YOUR_APP_SCOPES); -} - -function doBusinessLogic(auth) { +function renderApp(auth) { datastore.auth = auth; ReactDOM.render( @@ -131,6 +14,8 @@ function doBusinessLogic(auth) { , - document.getElementById("root") + document.getElementById('root') ); -} +}; + +window.addEventListener('DOMContentLoaded', () => initAuth(renderApp));