diff --git a/OHIFViewer/.meteor/packages b/OHIFViewer/.meteor/packages index db08c3217..5dcf596e4 100644 --- a/OHIFViewer/.meteor/packages +++ b/OHIFViewer/.meteor/packages @@ -25,7 +25,9 @@ reactive-var@1.0.11 reactive-dict@1.2.0 standard-minifier-css@1.4.1 standard-minifier-js@2.3.4 - +accounts-base +accounts-oauth +oauth-encryption # OHIF Packages ohif:polyfill @@ -42,6 +44,8 @@ ohif:dicom-services ohif:dicomweb-client ohif:hanging-protocols ohif:metadata +ohif:user +ohif:user-keycloak fortawesome:fontawesome momentjs:moment@2.15.1 diff --git a/OHIFViewer/.meteor/versions b/OHIFViewer/.meteor/versions index 0f7df10e8..c5ffbc870 100644 --- a/OHIFViewer/.meteor/versions +++ b/OHIFViewer/.meteor/versions @@ -1,4 +1,6 @@ accounts-base@1.4.2 +accounts-oauth@1.1.15 +accounts-password@1.5.1 aldeed:collection2@2.10.0 aldeed:collection2-core@1.2.0 aldeed:schema-deny@1.1.0 @@ -38,6 +40,7 @@ ecmascript-runtime@0.7.0 ecmascript-runtime-client@0.7.1 ecmascript-runtime-server@0.7.0 ejson@1.1.0 +email@1.2.3 es5-shim@4.8.0 fastclick@1.0.13 fortawesome:fontawesome@4.7.0 @@ -77,8 +80,15 @@ momentjs:moment@2.22.2 mongo@1.5.1 mongo-dev-server@1.1.0 mongo-id@1.0.7 +mxab:accounts-keycloak@0.0.2 +mxab:keycloak-loader@0.0.2 +mxab:keycloak-oauth@0.0.2 natestrauser:select2@4.0.3 +npm-bcrypt@0.9.3 npm-mongo@3.0.7 +oauth@1.2.3 +oauth-encryption@1.3.0 +oauth2@1.2.0 observe-sequence@1.0.16 ohif:commands@0.0.1 ohif:core@0.0.1 @@ -98,6 +108,8 @@ ohif:studies@0.0.1 ohif:study-list@0.0.1 ohif:themes@0.0.1 ohif:themes-common@0.0.1 +ohif:user@0.0.1 +ohif:user-keycloak@0.0.1 ohif:viewerbase@0.0.1 ohif:wadoproxy@0.0.1 ordered-dict@1.1.0 @@ -113,11 +125,13 @@ retry@1.1.0 routepolicy@1.0.13 service-configuration@1.0.11 session@1.1.7 +sha@1.0.9 shell-server@0.3.1 silentcicero:jszip@0.0.4 socket-stream-client@0.2.2 spacebars@1.0.15 spacebars-compiler@1.1.3 +srp@1.0.10 standard-app-packages@1.0.9 standard-minifier-css@1.4.1 standard-minifier-js@2.3.4 diff --git a/OHIFViewer/client/components/ohifViewer/ohifViewer.js b/OHIFViewer/client/components/ohifViewer/ohifViewer.js index 3a4c2b6bd..459946798 100644 --- a/OHIFViewer/client/components/ohifViewer/ohifViewer.js +++ b/OHIFViewer/client/components/ohifViewer/ohifViewer.js @@ -1,3 +1,4 @@ +import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; import { Session } from 'meteor/session'; import { Router } from 'meteor/clinical:router'; @@ -8,7 +9,7 @@ Template.ohifViewer.onCreated(() => { const instance = Template.instance(); instance.headerClasses = new ReactiveVar(''); - OHIF.header.dropdown.setItems([{ + const headerItems = [{ action: () => OHIF.ui.showDialog('serverInformationModal'), text: 'Server Information', icon: 'fa fa-server fa-lg', @@ -28,7 +29,18 @@ Template.ohifViewer.onCreated(() => { action: () => OHIF.ui.showDialog('aboutModal'), text: 'About', icon: 'fa fa-info' - }]); + }]; + + if (Meteor.user()) { + items.push({ + action: OHIF.user.logout, + text: 'Logout', + iconClasses: 'logout', + iconSvgUse: 'packages/ohif_viewerbase/assets/user-menu-icons.svg#logout' + }); + } + + OHIF.header.dropdown.setItems(headerItems); instance.autorun(() => { const currentRoute = Router.current(); diff --git a/OHIFViewer/client/config.js b/OHIFViewer/client/config.js index 0b1d9e0b5..49c7c9ce3 100644 --- a/OHIFViewer/client/config.js +++ b/OHIFViewer/client/config.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { OHIF } from 'meteor/ohif:core'; import { cornerstoneWADOImageLoader } from 'meteor/ohif:cornerstone'; +import { Accounts } from "meteor/accounts-base"; Meteor.startup(function() { const maxWebWorkers = Math.max(navigator.hardwareConcurrency - 1, 1); @@ -19,4 +20,21 @@ Meteor.startup(function() { }; cornerstoneWADOImageLoader.webWorkerManager.initialize(config); + + cornerstoneWADOImageLoader.configure({ + beforeSend: function(xhr) { + const userId = Meteor.userId(); + const accessToken = OHIF.user.getAccessToken(); + + if (accessToken) { + xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`); + } else { + const loginToken = Accounts._storedLoginToken(); + if (userId && loginToken) { + xhr.setRequestHeader("x-user-id", userId); + xhr.setRequestHeader("x-auth-token", loginToken); + } + } + } + }); }); diff --git a/Packages/ohif-dicomweb-client/src/getAccessToken.js b/Packages/ohif-dicomweb-client/src/getAccessToken.js new file mode 100644 index 000000000..b1da5273a --- /dev/null +++ b/Packages/ohif-dicomweb-client/src/getAccessToken.js @@ -0,0 +1,7 @@ +export default function getAccessToken() { + if (!global.window || !window.sessionStorage || !sessionStorage) { + return; + } + + return sessionStorage.token; +} diff --git a/Packages/ohif-dicomweb-client/src/index.js b/Packages/ohif-dicomweb-client/src/index.js index c7a0c1d78..cfa9358d5 100644 --- a/Packages/ohif-dicomweb-client/src/index.js +++ b/Packages/ohif-dicomweb-client/src/index.js @@ -6,6 +6,7 @@ import getModalities from './getModalities.js'; import getName from './getName.js'; import getNumber from './getNumber.js'; import getString from './getString.js'; +import getAccessToken from './getAccessToken.js'; import makeRequest from './makeRequest.js'; const DICOMWeb = { @@ -17,6 +18,7 @@ const DICOMWeb = { getName, getNumber, getString, + getAccessToken, makeRequest }; diff --git a/Packages/ohif-dicomweb-client/src/makeRequest.js b/Packages/ohif-dicomweb-client/src/makeRequest.js index 0d59d860a..9d6f5ec1e 100644 --- a/Packages/ohif-dicomweb-client/src/makeRequest.js +++ b/Packages/ohif-dicomweb-client/src/makeRequest.js @@ -1,6 +1,7 @@ import { Meteor } from "meteor/meteor"; import URL from 'url-parse'; import 'isomorphic-fetch'; +import getAccessToken from './getAccessToken.js'; async function makeRequest(url, options) { const parsed = new URL(url); @@ -10,8 +11,7 @@ async function makeRequest(url, options) { headers: {} }; - // TODO: Clean this up - const accessToken = false //Meteor.user().services.keycloak.accessToken; + const accessToken = getAccessToken(); if (accessToken) { requestOpt.headers = { Authorization: `Bearer ${accessToken}` diff --git a/Packages/ohif-servers/both/schema/servers.js b/Packages/ohif-servers/both/schema/servers.js index aa90dd1d5..71988598d 100644 --- a/Packages/ohif-servers/both/schema/servers.js +++ b/Packages/ohif-servers/both/schema/servers.js @@ -255,6 +255,11 @@ export const PublicServerConfig = new SimpleSchema({ label: 'Creates demo user on startup and show TestDrive button', defaultValue: true }, + userAuthenticationRoutesEnabled: { + type: Boolean, + label: 'Enables routing to /login page.', + defaultValue: false, + }, ui: { type: UISettings, label: 'UI Settings' diff --git a/Packages/ohif-studies/imports/both/services/qido/studies.js b/Packages/ohif-studies/imports/both/services/qido/studies.js index 24b39e1ae..b7ff4f97b 100644 --- a/Packages/ohif-studies/imports/both/services/qido/studies.js +++ b/Packages/ohif-studies/imports/both/services/qido/studies.js @@ -101,8 +101,6 @@ OHIF.studies.services.QIDO.Studies = (server, filter) => { const url = filterToQIDOURL(server, filter); return new Promise((resolve, reject) => { - console.warn(DICOMWeb); - DICOMWeb.getJSON(url, server.requestOptions).then(result => { const studies = resultDataToStudies(result); diff --git a/Packages/ohif-user-keycloak/client/main.js b/Packages/ohif-user-keycloak/client/main.js new file mode 100644 index 000000000..aacc070bd --- /dev/null +++ b/Packages/ohif-user-keycloak/client/main.js @@ -0,0 +1,10 @@ +import { Meteor } from 'meteor/meteor'; + +if (!Meteor.settings.public || + !Meteor.settings.public.custom || + !Meteor.settings.public.custom.keycloak) { + + console.log('To use the ohif-user-keycloak package, you must add relevant Keycloak settings to Meteor.settings.public.custom.keycloak (client-side).'); +} else { + require('../imports/client/index.js') +} diff --git a/Packages/ohif-user-keycloak/imports/client/components/keycloakLoginButton.html b/Packages/ohif-user-keycloak/imports/client/components/keycloakLoginButton.html new file mode 100644 index 000000000..3dfdfdba5 --- /dev/null +++ b/Packages/ohif-user-keycloak/imports/client/components/keycloakLoginButton.html @@ -0,0 +1,4 @@ + diff --git a/Packages/ohif-user-keycloak/imports/client/components/keycloakLoginButton.js b/Packages/ohif-user-keycloak/imports/client/components/keycloakLoginButton.js new file mode 100644 index 000000000..cba5b75d5 --- /dev/null +++ b/Packages/ohif-user-keycloak/imports/client/components/keycloakLoginButton.js @@ -0,0 +1,18 @@ +import { Meteor } from 'meteor/meteor'; +import { Template } from 'meteor/templating'; +import { Router } from 'meteor/clinical:router'; +import { OHIF } from 'meteor/ohif:core'; + +import './keycloakLoginButton.html'; + +Template.keycloakLoginButton.events({ + 'click .js-login-keycloak'() { + Meteor.loginWithMeteorKeycloak({}, function(error) { + if (error) { + throw new Error(error); + } + + Router.go('/studylist'); + }); + } +}); diff --git a/Packages/ohif-user-keycloak/imports/client/handlers.js b/Packages/ohif-user-keycloak/imports/client/handlers.js new file mode 100644 index 000000000..9d42ffd8a --- /dev/null +++ b/Packages/ohif-user-keycloak/imports/client/handlers.js @@ -0,0 +1,25 @@ +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from "meteor/meteor"; + +OHIF.user.getAccessToken = () => { + const user = Meteor.user(); + if (!user) { + return; + } + + return user.services.keycloak.accessToken; +}; + +Accounts.onLogin(() => { + Meteor.subscribe('user.services.keycloak', () => { + sessionStorage.token = OHIF.user.getAccessToken(); + }); +}); + +Accounts.onLogout(() => { + const authServerUrl = Meteor.settings.public.custom.keycloak.authServerUrl; + const realm = Meteor.settings.public.custom.keycloak.realmName; + const redirectUri = Meteor.absoluteUrl('login'); + const logoutUrl = `${authServerUrl}/realms/${realm}/protocol/openid-connect/logout?redirect_uri=${redirectUri}`; + window.location = logoutUrl; +}); diff --git a/Packages/ohif-user-keycloak/imports/client/index.js b/Packages/ohif-user-keycloak/imports/client/index.js new file mode 100644 index 000000000..fb5f07023 --- /dev/null +++ b/Packages/ohif-user-keycloak/imports/client/index.js @@ -0,0 +1,3 @@ +import './handlers'; +import './setup.js'; +import './components/keycloakLoginButton.js'; diff --git a/Packages/ohif-user-keycloak/imports/client/setup.js b/Packages/ohif-user-keycloak/imports/client/setup.js new file mode 100644 index 000000000..0d67f0d66 --- /dev/null +++ b/Packages/ohif-user-keycloak/imports/client/setup.js @@ -0,0 +1,30 @@ +import { Meteor } from 'meteor/meteor'; +import { Router } from 'meteor/clinical:router'; +import { OHIF } from 'meteor/ohif:core'; + +Router.waitOn(function() { + return [ + Meteor.subscribe('user.services.keycloak'), + ]; +}, { except: ['userLogin'] }); + +Router.onBeforeAction(function() { + // Check if user is signed in + if (!Meteor.userId() && !Meteor.loggingIn()) { + this.redirect('userLogin'); + } else { + this.next(); + } +}, { + except: ['userLogin', 'entrySignUp', 'forgotPassword', 'resetPassword'] +}); + +OHIF.user.additionalLoginButtons = OHIF.user.additionalLoginButtons || []; + +OHIF.user.additionalLoginButtons.push({ + template: 'keycloakLoginButton' +}); + +if (!Meteor.settings.public.userAuthenticationRoutesEnabled) { + OHIF.log.error('Please set Meteor.settings.public.userAuthenticationRoutesEnabled=true'); +} diff --git a/Packages/ohif-user-keycloak/imports/server/setup.js b/Packages/ohif-user-keycloak/imports/server/setup.js new file mode 100644 index 000000000..fbc95baaa --- /dev/null +++ b/Packages/ohif-user-keycloak/imports/server/setup.js @@ -0,0 +1,51 @@ +import { Meteor } from 'meteor/meteor'; +import { ServiceConfiguration } from 'meteor/service-configuration'; +import { Accounts } from "meteor/accounts-base"; + +console.log('Keycloak settings were found! Enabling Keycloak integration.'); + +ServiceConfiguration.configurations.upsert( + { service: 'keycloak' }, + { + $set: { + "realm": Meteor.settings.public.custom.keycloak.realmName, + "auth-server-url": Meteor.settings.public.custom.keycloak.authServerUrl, + "auth_redirect_uri": Meteor.settings.keycloak.authRedirectUri, + "ssl-required": Meteor.settings.keycloak.sslRequired, + "resource": Meteor.settings.keycloak.clientId, + "client_id": Meteor.settings.keycloak.clientId, + "loginStyle": Meteor.settings.keycloak.loginStyle, + "secret": Meteor.settings.keycloak.clientSecret, + "realm-public-key": Meteor.settings.keycloak.realmPublicKey, + "public-client": false, + "use-resource-role-mappings": false, + "bearer-only": false, + } + } +); + +Meteor.publish('user.services.keycloak', function() { + const userId = this.userId; + if (!userId) { + return []; + } + + return Meteor.users.find(userId, { + fields: { + 'services.keycloak': 1 + } + }); +}); + +Accounts.onLogout(({ user }) => { + if (!user) { + return; + } + + // Erase any Keycloak token that exists + Meteor.users.update(user._id, { + $unset: { + 'services.keycloak': 1 + } + }); +}); diff --git a/Packages/ohif-user-keycloak/package.js b/Packages/ohif-user-keycloak/package.js new file mode 100644 index 000000000..4a115a262 --- /dev/null +++ b/Packages/ohif-user-keycloak/package.js @@ -0,0 +1,26 @@ +Package.describe({ + name: 'ohif:user-keycloak', + summary: 'OHIF Integration with Keycloak for Identity and Access Management', + version: '0.0.1' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.6'); + + api.use('templating'); + api.use('ecmascript'); + api.use('service-configuration'); + api.use('accounts-base'); + + // Our custom packages + api.use('ohif:core'); + api.use('ohif:user'); + + api.use('mxab:keycloak-oauth@0.0.2'); + api.use('mxab:keycloak-loader@0.0.2'); + api.use('mxab:accounts-keycloak'); + + // Client imports + api.mainModule('client/main.js', 'client'); + api.mainModule('server/main.js', 'server'); +}); diff --git a/Packages/ohif-user-keycloak/server/main.js b/Packages/ohif-user-keycloak/server/main.js new file mode 100644 index 000000000..5965f9c25 --- /dev/null +++ b/Packages/ohif-user-keycloak/server/main.js @@ -0,0 +1,11 @@ +import { Meteor } from 'meteor/meteor'; + +if (!Meteor.settings.public || + !Meteor.settings.public.custom || + !Meteor.settings.public.custom.keycloak || + !Meteor.settings.keycloak) { + + console.log('To use the ohif-user-keycloak package, you must add relevant Keycloak settings to Meteor.settings.keycloak (server-side).'); +} else { + require('../imports/server/setup.js') +} diff --git a/Packages/ohif-user/client/components/login/userLogin.html b/Packages/ohif-user/client/components/login/userLogin.html index 02a9cc544..d9edf9a33 100644 --- a/Packages/ohif-user/client/components/login/userLogin.html +++ b/Packages/ohif-user/client/components/login/userLogin.html @@ -12,6 +12,10 @@ {{>section 'userLoginFieldsAfter'}}
{{#button action='login' class='form-control btn btn-primary'}}Enter{{/button}} + + {{ #each additionalLoginButtons }} + {{>Template.dynamic template=template}} + {{ /each }} {{/form}} {{>section 'userLoginAfter'}} diff --git a/Packages/ohif-user/client/components/login/userLogin.js b/Packages/ohif-user/client/components/login/userLogin.js index 6c94a9655..2ab368d89 100644 --- a/Packages/ohif-user/client/components/login/userLogin.js +++ b/Packages/ohif-user/client/components/login/userLogin.js @@ -33,3 +33,9 @@ Template.userLogin.onCreated(() => { instance.schema = OHIF.user.schema; }); + +Template.userLogin.helpers({ + additionalLoginButtons() { + return OHIF.user.additionalLoginButtons || []; + } +}) diff --git a/Packages/ohif-user/client/lib/getAccessToken.js b/Packages/ohif-user/client/lib/getAccessToken.js new file mode 100644 index 000000000..124fb717f --- /dev/null +++ b/Packages/ohif-user/client/lib/getAccessToken.js @@ -0,0 +1,5 @@ +import { Meteor } from 'meteor/meteor'; +import { OHIF } from 'meteor/ohif:core'; + +OHIF.user.getAccessToken = () => {}; + diff --git a/Packages/ohif-user/client/lib/index.js b/Packages/ohif-user/client/lib/index.js index 3853e390c..92995e73b 100644 --- a/Packages/ohif-user/client/lib/index.js +++ b/Packages/ohif-user/client/lib/index.js @@ -1,5 +1,6 @@ import './data'; import './getName'; +import './getAccessToken'; import './login'; import './logout'; import './schema'; diff --git a/Packages/ohif-user/client/routes.js b/Packages/ohif-user/client/routes.js index b1cf2ba37..492e139e6 100644 --- a/Packages/ohif-user/client/routes.js +++ b/Packages/ohif-user/client/routes.js @@ -1,6 +1,8 @@ import { Router } from 'meteor/clinical:router'; -Router.route('/login', function() { - this.layout('mainLayout', { data: {} }); - this.render('userLogin'); -}, { name: 'userLogin' }); +if (Meteor.settings.public.userAuthenticationRoutesEnabled === true) { + Router.route('/login', function() { + this.layout('layout', { data: {} }); + this.render('userLogin'); + }, { name: 'userLogin' }); +} diff --git a/Packages/ohif-viewerbase/assets/user-menu-icons.svg b/Packages/ohif-viewerbase/assets/user-menu-icons.svg new file mode 100644 index 000000000..94ab5818c --- /dev/null +++ b/Packages/ohif-viewerbase/assets/user-menu-icons.svg @@ -0,0 +1,26 @@ + + + Theme + + + + Log + + + + Server + + + + Study List + + + + Logout + + + + Password + + + \ No newline at end of file diff --git a/Packages/ohif-viewerbase/package.js b/Packages/ohif-viewerbase/package.js index 3bb3368af..0db14d0ab 100644 --- a/Packages/ohif-viewerbase/package.js +++ b/Packages/ohif-viewerbase/package.js @@ -29,6 +29,7 @@ Package.onUse(function(api) { const assets = [ 'assets/icons.svg', + 'assets/user-menu-icons.svg', 'assets/fonts/Roboto-Black-latin-ext.woff', 'assets/fonts/Roboto-Black-latin-ext.woff2', 'assets/fonts/Roboto-Black-latin.woff',