From e752c1da1c99661c02e8c9150d9f746c3b15fcdd Mon Sep 17 00:00:00 2001 From: Sandro Weber <webers@in.tum.de> Date: Tue, 12 Oct 2021 14:34:01 +0200 Subject: [PATCH] updated with newest changes form old frontend --- public/index.html | 1 + src/services/authentication-service-v2.js | 276 ++++++++++-------- .../files/experiment-storage-service.js | 2 - src/services/http-service.js | 9 +- src/services/proxy/nrp-user-service.js | 1 - src/services/roslib-service.js | 4 +- 6 files changed, 161 insertions(+), 132 deletions(-) diff --git a/public/index.html b/public/index.html index dd17215..7f0865c 100644 --- a/public/index.html +++ b/public/index.html @@ -28,6 +28,7 @@ <title>Neurorobotics Platform</title> </head> <body> + <script src="https://iam.ebrains.eu/auth/js/keycloak.js"></script> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <!-- diff --git a/src/services/authentication-service-v2.js b/src/services/authentication-service-v2.js index 85ff46f..11e4069 100644 --- a/src/services/authentication-service-v2.js +++ b/src/services/authentication-service-v2.js @@ -1,5 +1,12 @@ import config from '../config.json'; +/* global Keycloak */ + +let keycloakClient = undefined; + +const INIT_CHECK_INTERVAL_MS = 100; +const INIT_CHECK_MAX_RETRIES = 10; + let _instance = null; const SINGLETON_ENFORCER = Symbol(); @@ -12,13 +19,13 @@ class AuthenticationService { throw new Error('Use ' + this.constructor.name + '.instance'); } - this.CLIENT_ID = config.authV2.clientId; - this.CLIENT_SECRET = config.authV2.secret; - this.STORAGE_KEY = `tokens-${this.CLIENT_ID}@https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/auth`; + this.proxyURL = config.api.proxy.url; + this.oidcEnabled = config.auth.enableOIDC; + this.clientId = config.auth.clientId; + this.authURL = config.auth.url; + this.STORAGE_KEY = `tokens-${this.clientId}@https://iam.ebrains.eu/auth/realms/hbp`; - this.redirectToAuthPage = true; - this.checkForSessionStateAndAuthCode(); - //this.checkForNewTokenToStore(); + this.init(); } static get instance() { @@ -29,120 +36,126 @@ class AuthenticationService { return _instance; } - /** - * Checks if the current page URL contains access tokens. - * This happens when the successfully logging in at the proxy login page and - * being redirected back with the token info. - * Will automatically remove additional access info and present a clean URL after being redirected. - */ - checkForNewTokenToStore() { - const path = window.location.href; - const accessTokenMatch = /&access_token=([^&]*)/.exec(path); + init() { + this.initialized = false; + if (this.oidcEnabled) { + this.authCollab().then(() => { + this.initialized = true; + }); + } + else { + this.checkForNewLocalTokenToStore(); + this.initialized = true; + } - if (!accessTokenMatch || !accessTokenMatch[1]) { - return; + this.promiseInitialized = new Promise((resolve, reject) => { + let numChecks = 0; + let checkInterval = setInterval(() => { + numChecks++; + if (numChecks > INIT_CHECK_MAX_RETRIES) { + clearInterval(checkInterval); + reject(); + } + if (this.initialized) { + clearInterval(checkInterval); + resolve(); + } + }, INIT_CHECK_INTERVAL_MS); + }); + } + + authenticate(config) { + if (this.oidcEnabled) { + this.authCollab(config); } + else { + this.authLocal(config); + } + } - let accessToken = accessTokenMatch[1]; + getToken() { + if (this.oidcEnabled) { + if (keycloakClient && keycloakClient.authenticated) { + keycloakClient + .updateToken(30) + .then(function() {}) + .catch(function() { + console.error('Failed to refresh token'); + }); + return keycloakClient.token; + } + else { + console.error('getToken() - Client is not authenticated'); + } + } + else { + return this.getStoredLocalToken(); + } + } - localStorage.setItem( - this.STORAGE_KEY, - //eslint-disable-next-line camelcase - JSON.stringify([{ access_token: accessToken }]) - ); - const pathMinusAccessToken = path.substr(0, path.indexOf('&access_token=')); - window.location.href = pathMinusAccessToken; + logout() { + if (this.oidcEnabled) { + if (keycloakClient && keycloakClient.authenticated) { + keycloakClient.logout(); + keycloakClient.clearStoredLocalToken(); + } + else { + console.error('Client is not authenticated'); + } + } + else { + return this.clearStoredLocalToken(); + } } - /** - * Checks if the current page URL contains access tokens. - * This happens when the successfully logging in at the proxy login page and - * being redirected back with the token info. - * Will automatically remove additional access info and present a clean URL after being redirected. - */ - checkForSessionStateAndAuthCode() { - const path = window.location.href; - const sessionStateMatch = /session_state=([^&]*)/.exec(path); - const authCodeMatch = /code=([^&]*)/.exec(path); - - if (!sessionStateMatch || !authCodeMatch) { + authLocal(config) { + if (this.authenticating) { return; } + this.authenticating = true; - this.redirectToAuthPage = false; + this.authURL = this.authURL || config.url; + this.clientId = this.clientId || config.clientId; - let sessionState = sessionStateMatch[1]; - let authCode = authCodeMatch[1]; - console.info('authCode = ' + authCode); + let absoluteUrl = /^https?:\/\//i; + if (!absoluteUrl.test(this.authURL)) { + this.authURL = `${this.proxyURL}${this.authURL}`; + } - this.getAccessToken(authCode); + this.clearStoredLocalToken(); + window.location.href = `${this.authURL}&client_id=${this + .clientId}&redirect_uri=${encodeURIComponent(window.location.href)}`; } - async getAccessToken(authorizationCode) { - console.info('getAccessToken - origin = ' + window.location.origin); - console.info('getAccessToken - authenticationCode = ' + authorizationCode); - let urlRequestAccessToken = 'https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/token'; - - let options = { - method: 'POST', - mode: 'cors', // no-cors, *cors, same-origin - //cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached - //credentials: 'same-origin', // include, *same-origin, omit - headers: { - 'Content-Type': 'application/x-www-form-urlencoded'//, - //'Access-Control-Allow-Origin': '*', - //Referer: window.location.origin - }, - // redirect: manual, *follow, error - //redirect: 'follow', - // referrerPolicy: no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, - // strict-origin, strict-origin-when-cross-origin, unsafe-url - referrerPolicy: 'no-referrer' - }; - - let formDetails = { - grant_type: 'authorization_code', - client_id: this.CLIENT_ID, - redirect_uri: window.location.origin, - client_secret: this.CLIENT_SECRET, - code: authorizationCode - }; - const formBody = Object.entries(formDetails) - .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)) - .join('&'); - options.body = formBody; - - const responseAccessTokenRequest = await fetch(urlRequestAccessToken, options); - const responseAccessTokenRequestJSON = await responseAccessTokenRequest.json(); - console.info(responseAccessTokenRequest); - console.info(responseAccessTokenRequestJSON); - - /*localStorage.setItem( + checkForNewLocalTokenToStore() { + const path = window.location.pathname; + + const accessTokenMatch = /&access_token=([^&]*)/.exec(path); + if (!accessTokenMatch || !accessTokenMatch[1]) { + return; + } + + let accessToken = accessTokenMatch[1]; + localStorage.setItem( this.STORAGE_KEY, //eslint-disable-next-line camelcase JSON.stringify([{ access_token: accessToken }]) - );*/ - //const pathMinusAccessToken = path.substr(0, path.indexOf('?')); - //window.location.href = pathMinusAccessToken; + ); + + // navigate to clean url + let cleanedPath = path.substr(0, path.indexOf('&')); + window.location = cleanedPath; } - /** - * Clear currently stored access token. - */ - clearStoredToken() { + clearStoredLocalToken() { localStorage.removeItem(this.STORAGE_KEY); } - /** - * Get the stored access token. - * - * @return token The stored access token. Or strings identifying 'no-token' / 'malformed-token'. - */ - getStoredToken() { + getStoredLocalToken() { let storedItem = localStorage.getItem(this.STORAGE_KEY); if (!storedItem) { // this token will be rejected by the server and the client will get a proper auth error - return AuthenticationService.CONSTANTS.NO_TOKEN; + return 'no-token'; } try { @@ -151,38 +164,57 @@ class AuthenticationService { } catch (e) { // this token will be rejected by the server and the client will get a proper auth error - return AuthenticationService.CONSTANTS.MALFORMED_TOKEN; + return 'malformed-token'; } } - /** - * Opens the proxy's authentication page. - * - * @param {*} url The URL of the authentication page. - * If not an absolute URL it is assumed to be a subpage of the proxy. - */ - openAuthenticationPage(url = config.authV2.url) { - if (!this.redirectToAuthPage) { + authCollab(config) { + if (this.authenticating) { return; } - console.info('openAuthenticationPage - origin=' + window.location.origin); - - this.clearStoredToken(); + this.authenticating = true; + + return new Promise(resolve => { + this.authURL = this.authURL || config.url; + + this.initKeycloakClient().then(() => { + if (!keycloakClient.authenticated) { + // User is not authenticated, run login + keycloakClient + .login({ scope: 'openid profile email group' }) + .then(() => { + resolve(true); + }); + } + else { + keycloakClient.loadUserInfo().then(userInfo => { + this.userInfo = userInfo; + resolve(true); + }); + } + }); + }); + } - let absoluteUrl = /^https?:\/\//i; - if (!absoluteUrl.test(url)) { - url = `https://${url}`; - } - /*window.location.href = `${url} - &client_id=${this.CLIENT_ID} - &redirect_uri=${encodeURIComponent(window.location.href)}`;*/ - - let testClientID = 'community-apps-tutorial'; - console.info('redirect_uri=' + window.location.origin); - let testRedirectURI = window.location.origin; //'http://localhost:3000'; - window.location.href = url + - '&client_id=' + testClientID + - '&redirect_uri=' + testRedirectURI; + initKeycloakClient() { + return new Promise(resolve => { + keycloakClient = Keycloak({ + realm: 'hbp', + clientId: this.clientId, + //'public-client': true, + 'confidential-port': 0, + url: this.authURL, + redirectUri: window.location.href // 'http://localhost:9001/#/esv-private' // + }); + + keycloakClient + .init({ + flow: 'hybrid' /*, responseMode: 'fragment'*/ + }) + .then(() => { + resolve(keycloakClient); + }); + }); } } diff --git a/src/services/experiments/files/experiment-storage-service.js b/src/services/experiments/files/experiment-storage-service.js index 8d63263..ac20e1a 100644 --- a/src/services/experiments/files/experiment-storage-service.js +++ b/src/services/experiments/files/experiment-storage-service.js @@ -253,13 +253,11 @@ class ExperimentStorageService extends HttpService { async setFile(directoryPath, filename, data, byname = true, contentType = 'text/plain') { let directory = directoryPath.replaceAll('/', '%2F'); const url = new URL(`${config.api.proxy.url}${endpoints.proxy.storage.url}/${directory}/${filename}`); - //console.info(url); url.searchParams.append('byname', byname); let requestOptions = { ...this.POSTOptions, ...{ headers: { 'Content-Type': contentType } } }; - //console.info(requestOptions); if (contentType === 'text/plain') { return this.httpRequestPOST(url, data, requestOptions); diff --git a/src/services/http-service.js b/src/services/http-service.js index 30651bc..87abf46 100644 --- a/src/services/http-service.js +++ b/src/services/http-service.js @@ -44,7 +44,9 @@ export class HttpService extends EventEmitter { */ performRequest = async (url, options, data) => { // Add authorization header - options.headers.Authorization = `Bearer ${AuthenticationService.instance.getStoredToken()}`; + await AuthenticationService.instance.promiseInitialized; + let token = AuthenticationService.instance.getToken(); + options.headers.Authorization = 'Bearer ' + token; if (data) { options.body = data; } @@ -54,10 +56,7 @@ export class HttpService extends EventEmitter { // error handling if (!response.ok) { if (response.status === 477) { - const responseText = await response.text(); - console.info('auth error'); - console.info(responseText); - AuthenticationService.instance.openAuthenticationPage(/*responseText*/); + AuthenticationService.instance.authenticate(); } else if (response.status === 478) { //TODO: redirect to maintenance page diff --git a/src/services/proxy/nrp-user-service.js b/src/services/proxy/nrp-user-service.js index 94f0024..08b6af6 100644 --- a/src/services/proxy/nrp-user-service.js +++ b/src/services/proxy/nrp-user-service.js @@ -62,7 +62,6 @@ class NrpUserService extends HttpService { async getCurrentUser() { if (!this.currentUser) { let responseIdentity = await this.httpRequestGET(IDENTITY_ME_URL); - console.info(responseIdentity); if (responseIdentity.ok) { this.currentUser = await responseIdentity.json(); } diff --git a/src/services/roslib-service.js b/src/services/roslib-service.js index ad6c364..aa32447 100644 --- a/src/services/roslib-service.js +++ b/src/services/roslib-service.js @@ -1,7 +1,7 @@ import * as ROSLIB from 'roslib'; import _ from 'lodash'; -import AuthenticationService from './authentication-service.js'; +import AuthenticationService from './authentication-service-v2'; let _instance = null; const SINGLETON_ENFORCER = Symbol(); @@ -32,7 +32,7 @@ class RoslibService { */ getConnection(url) { if (!this.connections.has(url)) { - let urlWithAuth = url + '?token=' + AuthenticationService.instance.getStoredToken(); + let urlWithAuth = url + '?token=' + AuthenticationService.instance.getToken(); this.connections.set(url, new ROSLIB.Ros({ url: urlWithAuth })); } -- GitLab