Skip to content
Snippets Groups Projects
Commit 5c97fe1f authored by Sandro Weber's avatar Sandro Weber
Browse files

Merged in auth-v2 (pull request #31)

Auth v2

Approved-by: Manos Angelidis
parents 183eba67 e8d9d921
No related branches found
No related tags found
No related merge requests found
......@@ -45,6 +45,7 @@
},
"scripts": {
"start": "react-scripts start",
"startHTTPS": "HTTPS=true react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
......
......@@ -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>
<!--
......
import React from 'react';
import { HashRouter, Switch, Route } from 'react-router-dom';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import EntryPage from './components/entry-page/entry-page';
import ErrorDialog from './components/dialog/error-dialog.js';
......@@ -14,13 +14,13 @@ class App extends React.Component {
<div>
<ErrorDialog />
<NotificationDialog/>
<HashRouter>
<BrowserRouter>
<Switch>
<Route path='/experiments-overview' component={ExperimentOverview} />
<Route path='/simulation-view/:serverIP/:simulationID' component={SimulationView} />
<Route path='/' component={EntryPage} />
</Switch>
</HashRouter>
</BrowserRouter>
</div>
);
}
......
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,11 +19,13 @@ class AuthenticationService {
throw new Error('Use ' + this.constructor.name + '.instance');
}
this.CLIENT_ID = config.auth.clientId;
this.STORAGE_KEY = `tokens-${this.CLIENT_ID}@https://services.humanbrainproject.eu/oidc`;
this.PROXY_URL = config.api.proxy.url;
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.checkForNewTokenToStore();
this.init();
}
static get instance() {
......@@ -27,44 +36,122 @@ 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;
}
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);
}
}
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();
}
}
logout() {
if (this.oidcEnabled) {
if (keycloakClient && keycloakClient.authenticated) {
keycloakClient.logout();
keycloakClient.clearStoredLocalToken();
}
else {
console.error('Client is not authenticated');
}
}
else {
return this.clearStoredLocalToken();
}
}
authLocal(config) {
if (this.authenticating) {
return;
}
this.authenticating = true;
this.authURL = this.authURL || config.url;
this.clientId = this.clientId || config.clientId;
let absoluteUrl = /^https?:\/\//i;
if (!absoluteUrl.test(this.authURL)) {
this.authURL = `${this.proxyURL}${this.authURL}`;
}
this.clearStoredLocalToken();
window.location.href = `${this.authURL}&client_id=${this
.clientId}&redirect_uri=${encodeURIComponent(window.location.href)}`;
}
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('&access_token='));
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
......@@ -77,31 +164,63 @@ 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) {
this.clearStoredToken();
let absoluteUrl = /^https?:\/\//i;
if (!absoluteUrl.test(url)) {
url = `${this.PROXY_URL}${url}`;
authCollab(config) {
if (this.authenticating) {
return;
}
window.location.href = `${url}&client_id=${
this.CLIENT_ID
}&redirect_uri=${encodeURIComponent(window.location.href)}`;
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);
});
}
});
});
}
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);
});
});
}
}
AuthenticationService.CONSTANTS = Object.freeze({
MALFORMED_TOKEN: 'malformed-token'
MALFORMED_TOKEN: 'malformed-token',
NO_TOKEN: 'no-token'
});
export default AuthenticationService;
......@@ -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);
......
import { EventEmitter } from 'events';
import AuthenticationService from './authentication-service.js';
import AuthenticationService from './authentication-service';
/**
* Base class that performs http requests with default request options.
......@@ -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,8 +56,7 @@ export class HttpService extends EventEmitter {
// error handling
if (!response.ok) {
if (response.status === 477) {
const responseText = await response.text();
AuthenticationService.instance.openAuthenticationPage(responseText);
AuthenticationService.instance.authenticate();
}
else if (response.status === 478) {
//TODO: redirect to maintenance page
......
......@@ -61,7 +61,10 @@ class NrpUserService extends HttpService {
*/
async getCurrentUser() {
if (!this.currentUser) {
this.currentUser = await (await this.httpRequestGET(IDENTITY_ME_URL)).json();
let responseIdentity = await this.httpRequestGET(IDENTITY_ME_URL);
if (responseIdentity.ok) {
this.currentUser = await responseIdentity.json();
}
}
return this.currentUser;
......
import * as ROSLIB from 'roslib';
import _ from 'lodash';
import AuthenticationService from './authentication-service.js';
import AuthenticationService from './authentication-service';
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 }));
}
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment