Add OAuth support

This commit is contained in:
Erik Ziegler 2019-01-07 19:17:10 +01:00
parent 0cbf15c46e
commit 9e7314e4a5
16 changed files with 273 additions and 10531 deletions

3
OHIFViewer-react/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
src/sha.js
src/version.js
yarn-error.log

View File

@ -14,6 +14,7 @@
"lodash.debounce": "^4.0.8",
"moment": "^2.22.2",
"ohif-core": "^0.1.3",
"oidc-client": "^1.6.1",
"prop-types": "^15.6.2",
"query-string": "^6.2.0",
"react": "^16.4.1",
@ -24,7 +25,8 @@
"react-router-dom": "^4.3.1",
"react-scripts": "^2.1.1",
"react-viewerbase": "^0.1.9",
"redux": "^4.0.1"
"redux": "^4.0.1",
"redux-oidc": "^3.1.0"
},
"scripts": {
"copy:webworkers": "cp node_modules/cornerstone-wado-image-loader/dist/*.min.js* public",

12
OHIFViewer-react/public/es6-shim.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OpenID Connect Logout Redirect Page</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.7/es5-shim.min.js"></script>
<script type="text/javascript" src='es6-shim.min.js'></script>
<script type="text/javascript" src='oidc-client.min.js'></script>
<script type="text/javascript" src='polyfill.min.js'></script>
<script type="text/javascript">
// TODO: I think this is actually not necessary anymore
new UserManager().signoutRedirectCallback().then(function() {
// TODO: Keycloak + Auth0 is not properly logging out both sessions?
// This is a hacky hardcoded workaround. This could also be added as a component to the React application rather than a static page.
var path = 'https://www.crowds-cure.org';
var client_id = 'z5cXMPTxeFOdB3i4xRA8JyhTonQmqMKM';
var url = 'https://auth.crowds-cure.org/v2/logout?client_id=' + client_id + '&returnTo=' + path;
window.location.href = url;
});
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n():"function"==typeof define&&define.amd?define(n):n()}(0,function(){"use strict";function e(e){var n=this.constructor;return this.then(function(t){return n.resolve(e()).then(function(){return t})},function(t){return n.resolve(e()).then(function(){return n.reject(t)})})}function n(){}function t(e){if(!(this instanceof t))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],u(e,this)}function o(e,n){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,t._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null!==t){var o;try{o=t(e._value)}catch(f){return void i(n.promise,f)}r(n.promise,o)}else(1===e._state?r:i)(n.promise,e._value)})):e._deferreds.push(n)}function r(e,n){try{if(n===e)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var o=n.then;if(n instanceof t)return e._state=3,e._value=n,void f(e);if("function"==typeof o)return void u(function(e,n){return function(){e.apply(n,arguments)}}(o,n),e)}e._state=1,e._value=n,f(e)}catch(r){i(e,r)}}function i(e,n){e._state=2,e._value=n,f(e)}function f(e){2===e._state&&0===e._deferreds.length&&t._immediateFn(function(){e._handled||t._unhandledRejectionFn(e._value)});for(var n=0,r=e._deferreds.length;r>n;n++)o(e,e._deferreds[n]);e._deferreds=null}function u(e,n){var t=!1;try{e(function(e){t||(t=!0,r(n,e))},function(e){t||(t=!0,i(n,e))})}catch(o){if(t)return;t=!0,i(n,o)}}var c=setTimeout;t.prototype["catch"]=function(e){return this.then(null,e)},t.prototype.then=function(e,t){var r=new this.constructor(n);return o(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(e,t,r)),r},t.prototype["finally"]=e,t.all=function(e){return new t(function(n,t){function o(e,f){try{if(f&&("object"==typeof f||"function"==typeof f)){var u=f.then;if("function"==typeof u)return void u.call(f,function(n){o(e,n)},t)}r[e]=f,0==--i&&n(r)}catch(c){t(c)}}if(!e||"undefined"==typeof e.length)throw new TypeError("Promise.all accepts an array");var r=Array.prototype.slice.call(e);if(0===r.length)return n([]);for(var i=r.length,f=0;r.length>f;f++)o(f,r[f])})},t.resolve=function(e){return e&&"object"==typeof e&&e.constructor===t?e:new t(function(n){n(e)})},t.reject=function(e){return new t(function(n,t){t(e)})},t.race=function(e){return new t(function(n,t){for(var o=0,r=e.length;r>o;o++)e[o].then(n,t)})},t._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){c(e,0)},t._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var l=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"Promise"in l?l.Promise.prototype["finally"]||(l.Promise.prototype["finally"]=e):l.Promise=t});

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Silent OpenID Connect Token Refresh Page</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.7/es5-shim.min.js"></script>
<script type="text/javascript" src='es6-shim.min.js'></script>
<script type="text/javascript" src="oidc-client.min.js"></script>
<script type="text/javascript" src='polyfill.min.js'></script>
<script type="text/javascript">
new UserManager().signinSilentCallback().catch(console.error);
</script>
</body>
</html>

View File

@ -1,12 +1,14 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { Route, Switch } from 'react-router-dom';
import { connect } from 'react-redux';
import { ViewerbaseDragDropContext } from 'react-viewerbase';
import ViewerRouting from "./ViewerRouting.js";
import StudyListRouting from './StudyListRouting.js';
import StandaloneRouting from './StandaloneRouting.js';
import IHEInvokeImageDisplay from './IHEInvokeImageDisplay.js';
import { ViewerbaseDragDropContext } from 'react-viewerbase';
import CallbackPage from './CallbackPage.js';
import userManager from "./userManager.js";
import './App.css';
import './variables.css';
import './theme-tide.css';
@ -24,11 +26,6 @@ function setContext(context) {
class App extends Component {
componentDidMount() {
// Temporary until the rest of the UI is in React
window.router = {
history: this.props.history
};
this.unlisten = this.props.history.listen((location, action) => {
setContext(window.location.pathname);
});
@ -39,50 +36,50 @@ class App extends Component {
}
render() {
const user = this.props.user;
if (!user || user.expired) {
// TODO: redirect to OAuth page if necessary
//return ('Loading user...');
}
return (
<Switch>
<Route
exact
path="/studylist"
component={StudyListRouting}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
exact
path="/"
component={StudyListRouting}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
exact
path="/viewer"
component={StandaloneRouting}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
path="/viewer/:studyInstanceUids"
component={ViewerRouting}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
path="/study/:studyInstanceUid/series/:seriesInstanceUids"
component={ViewerRouting}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
path="/IHEInvokeImageDisplay"
component={IHEInvokeImageDisplay}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route path="/silent-refresh.html" onEnter={reload} />
<Route path="/logout-redirect.html" onEnter={reload} />
<Route exact path='/login' component={() => {
userManager.signinRedirect();
}}
/>
<Route path="/callback" component={CallbackPage} />
<Route render={() =>
<div> Sorry, this page does not exist. </div>}
/>
@ -91,4 +88,15 @@ class App extends Component {
}
}
export default ViewerbaseDragDropContext(withRouter(App));
const mapStateToProps = state => {
return {
user: state.oidc.user,
};
};
const ConnectedApp = connect(
mapStateToProps,
null
)(App);
export default ViewerbaseDragDropContext(withRouter(ConnectedApp));

View File

@ -0,0 +1,24 @@
import React from "react";
import { CallbackComponent } from "redux-oidc";
import { withRouter } from "react-router-dom";
import userManager from "./userManager";
class CallbackPage extends React.Component {
render() {
// just redirect to '/' in both cases
return (
<CallbackComponent
userManager={userManager}
successCallback={() => this.props.history.push("/")}
errorCallback={error => {
this.props.history.push("/");
console.error(error);
}}
>
<div>Redirecting...</div>
</CallbackComponent>
);
}
}
export default withRouter(CallbackPage);

View File

@ -4,12 +4,12 @@ import StudyListWithData from './StudyListWithData.js';
const isActive = (a) => a.active === true;
const mapStateToProps = (state, ownProps) => {
const mapStateToProps = state => {
const activeServer = state.servers.servers.find(isActive);
return {
server: activeServer,
...ownProps
user: state.oidc.user,
};
};

View File

@ -15,7 +15,7 @@ class StudyListWithData extends Component {
error: null
};
this.pageSize = 5;
this.rowsPerPage = 20;
this.defaultSort = { field: 'patientName', order: 'desc' };
}
@ -28,18 +28,17 @@ class StudyListWithData extends Component {
searchForStudies = (searchData = {
currentPage: 0,
pageSize: 5
rowsPerPage: this.rowsPerPage
}) => {
const { server } = this.props;
console.log(searchData);
const filter = {
patientId: searchData.patientId,
patientName: searchData.patientName,
accessionNumber: searchData.accessionNumber,
studyDescription: searchData.studyDescription,
modalitiesInStudy: searchData.modalitiesInStudy,
limit: searchData.currentPage * searchData.pageSize + searchData.pageSize,
offset: searchData.currentPage * searchData.pageSize
limit: searchData.currentPage * searchData.rowsPerPage + searchData.rowsPerPage,
offset: searchData.currentPage * searchData.rowsPerPage
};
// TODO: add sorting
@ -98,13 +97,13 @@ class StudyListWithData extends Component {
const studyCount = this.state.studies ? this.state.studies.length : 0;
return (<>
<Header home />
<Header home user={this.props.user}/>
<StudyList studies={this.state.studies}
studyCount={studyCount}
studyListFunctionsEnabled={false}
onImport={this.onImport}
onSelectItem={this.onSelectItem}
pageSize={this.pageSize}
pageSize={this.rowsPerPage}
defaultSort={this.defaultSort}
onSearch={this.onSearch} />
</>
@ -114,7 +113,8 @@ class StudyListWithData extends Component {
StudyListWithData.propTypes = {
patientId: PropTypes.string,
server: PropTypes.object
server: PropTypes.object,
user: PropTypes.object
};
export default withRouter(StudyListWithData);

View File

@ -54,6 +54,17 @@ const config = {
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
OHIF.user.getAccessToken = () => {
// TODO: Get the Redux store from somewhere else
const state = window.store.getState();
if (!state.oidc || !state.oidc.user) {
return;
}
return state.oidc.user.access_token;
}
cornerstoneWADOImageLoader.configure({
beforeSend: function(xhr) {
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
@ -67,7 +78,7 @@ cornerstoneWADOImageLoader.configure({
// Set the tool font and font size
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
const fontFamily =
'Work Sans, Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`);
// Set the tool width

View File

@ -3,17 +3,23 @@ import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import App from './App.js';
import { loadUser, reducer as oidcReducer, OidcProvider} from 'redux-oidc';
import OHIF from 'ohif-core';
import './config';
import App from './App.js';
import ui from './redux/ui.js'
import userManager from './userManager.js';
import Icons from "./images/icons.svg"
const reducers = OHIF.redux.reducers;
reducers.ui = ui;
reducers.oidc = oidcReducer;
const combined = combineReducers(reducers)
const store = createStore(combined);
loadUser(store, userManager);
const defaultButtons = [
{
@ -96,6 +102,9 @@ const servers = {
"wadoUriRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/wado",
"qidoRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/rs",
"wadoRoot": "https://dcm4che.ohif.club/dcm4chee-arc/aets/DCM4CHEE/rs",
//"wadoUriRoot": "https://cancer.crowds-cure.org/dcm4chee-arc/aets/DCM4CHEE/wado",
//"qidoRoot": "https://cancer.crowds-cure.org/dcm4chee-arc/aets/DCM4CHEE/rs",
//"wadoRoot": "https://cancer.crowds-cure.org/dcm4chee-arc/aets/DCM4CHEE/rs",
"qidoSupportsIncludeField": true,
"imageRendering": "wadors",
"thumbnailRendering": "wadors",
@ -118,9 +127,11 @@ window.store = store;
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App store={store}/>
</BrowserRouter>
<OidcProvider store={store} userManager={userManager}>
<BrowserRouter>
<App/>
</BrowserRouter>
</OidcProvider>
</Provider>,
document.getElementById('root')
);

View File

@ -0,0 +1,34 @@
import { createUserManager } from 'redux-oidc';
const oidcClient = {
authServerUrl: 'https://cancer.crowds-cure.org/auth/realms/dcm4che',
clientId: 'crowds-cure-cancer',
authRedirectUri: '/callback',
postLogoutRedirectUri: '/logout-redirect.html',
responseType: 'id_token token',
scope: 'email profile openid',
revokeAccessTokenOnSignout: true,
extraQueryParams: {
kc_idp_hint: 'crowds-cure-cancer-auth0-oidc',
client_id: 'crowds-cure-cancer'
}
};
const settings = {
authority: oidcClient.authServerUrl,
client_id: oidcClient.clientId,
redirect_uri: oidcClient.authRedirectUri,
silent_redirect_uri: '/silent-refresh.html',
post_logout_redirect_uri: oidcClient.postLogoutRedirectUri,
response_type: oidcClient.responseType,
scope: 'email profile openid', // Note: Request must have scope 'openid' to be considered an OpenID Connect request
automaticSilentRenew: true,
revokeAccessTokenOnSignout: true,
filterProtocolClaims: true,
loadUserInfo: true,
extraQueryParams: oidcClient.extraQueryParams
};
const userManager = createUserManager(settings);
export default userManager;

File diff suppressed because it is too large Load Diff

View File

@ -1604,6 +1604,15 @@ babel-plugin-transform-react-remove-prop-types@0.4.18:
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.18.tgz#85ff79d66047b34288c6f7cc986b8854ab384f8c"
integrity sha512-azed2nHo8vmOy7EY26KH+om5oOcWRs0r1U8wOmhwta+SBMMnmJ4H6yaBZRCcHBtMeWp9AVhvBTL/lpR1kEx+Xw==
babel-polyfill@>=6.9.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153"
integrity sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=
dependencies:
babel-runtime "^6.26.0"
core-js "^2.5.0"
regenerator-runtime "^0.10.5"
babel-preset-jest@^23.2.0:
version "23.2.0"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46"
@ -4735,6 +4744,11 @@ immer@1.7.2:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.7.2.tgz#a51e9723c50b27e132f6566facbec1c85fc69547"
integrity sha512-4Urocwu9+XLDJw4Tc6ZCg7APVjjLInCFvO4TwGsAYV5zT6YYSor14dsZR0+0tHlDIN92cFUOq+i7fC00G5vTxA==
immutable@>=3.6.0:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@ -5861,6 +5875,11 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jsrsasign@^8.0.12:
version "8.0.12"
resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.12.tgz#22abb9656d34a30b9530436720835e89c2e5c316"
integrity sha1-Iqu5ZW00owuVMENnIINeicLlwxY=
jsx-ast-utils@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
@ -6961,6 +6980,15 @@ ohif-core@^0.1.3:
simpl-schema "^1.5.3"
underscore "^1.9.1"
oidc-client@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/oidc-client/-/oidc-client-1.6.1.tgz#76a7f24ad7fb5342269b6cae895bb32e1e27e785"
integrity sha512-buA9G0hlFjUwxoL/xuHunBtTgoICtJSojnZtATqMliUTKxPYAzHprOQ85Lj0hgF+Zv8lI/ViqaNFDG0Z5KCUKA==
dependencies:
jsrsasign "^8.0.12"
optionalDependencies:
babel-polyfill ">=6.9.1"
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@ -8485,10 +8513,10 @@ react-transition-group@^2.0.0, react-transition-group@^2.2.0:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-viewerbase@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/react-viewerbase/-/react-viewerbase-0.1.8.tgz#982d25572a4b4ebf8f251e6770de23364421bff6"
integrity sha512-bUg/bO6EQhbE6kEkfw1n91GTjSy52xP6BnfESSUEz9JcZ0VmcG8cDKhNSc+JVvtAAdjmmhLgCEkpoL9uza+CPQ==
react-viewerbase@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/react-viewerbase/-/react-viewerbase-0.1.9.tgz#9d6930cc22149dd34bbf5d088218f86a804f0260"
integrity sha512-0wpEATJxdZAUT8dqUJa4Q0FSv7Zo+HLh969uDXx+x9ZtlZi2JgsU8Hb97D4ump16LHfg6OG5uQqJPcQzrDGWYA==
dependencies:
classnames "^2.2.6"
react-bootstrap-modal "^4.2.0"
@ -8606,6 +8634,13 @@ recursive-readdir@2.2.2:
dependencies:
minimatch "3.0.4"
redux-oidc@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-oidc/-/redux-oidc-3.1.0.tgz#26791fab7d2542f4827a175d515a941a5e35b4c6"
integrity sha512-VygfGdkuUI8kRn+R1LQLcGAmm0fJUPHKqaRB3l2BL8D9s3sIFFanuVf7Q05QS6DlRVIz9pmcgInyBHlAVZE8og==
optionalDependencies:
immutable ">=3.6.0"
redux@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5"
@ -8626,6 +8661,11 @@ regenerate@^1.2.1, regenerate@^1.4.0:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
regenerator-runtime@^0.10.5:
version "0.10.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
integrity sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"