feat(keycloak) Optional Keycloak integration via OpenId Connect (OIDC) for Identity and Access Management (#227)

This commit is contained in:
Erik Ziegler 2018-07-19 13:57:23 +02:00 committed by GitHub
parent 03d0798459
commit 33755f7b8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 294 additions and 11 deletions

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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);
}
}
}
});
});

View File

@ -0,0 +1,7 @@
export default function getAccessToken() {
if (!global.window || !window.sessionStorage || !sessionStorage) {
return;
}
return sessionStorage.token;
}

View File

@ -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
};

View File

@ -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}`

View File

@ -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'

View File

@ -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);

View File

@ -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')
}

View File

@ -0,0 +1,4 @@
<template name="keycloakLoginButton">
<hr/>
<button class="form-control btn js-login-keycloak">Login with Keycloak</button>
</template>

View File

@ -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');
});
}
});

View File

@ -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;
});

View File

@ -0,0 +1,3 @@
import './handlers';
import './setup.js';
import './components/keycloakLoginButton.js';

View File

@ -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');
}

View File

@ -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
}
});
});

View File

@ -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');
});

View File

@ -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')
}

View File

@ -12,6 +12,10 @@
{{>section 'userLoginFieldsAfter'}}
<hr class="m-t-0 m-b-2">
{{#button action='login' class='form-control btn btn-primary'}}Enter{{/button}}
{{ #each additionalLoginButtons }}
{{>Template.dynamic template=template}}
{{ /each }}
{{/form}}
</div>
{{>section 'userLoginAfter'}}

View File

@ -33,3 +33,9 @@ Template.userLogin.onCreated(() => {
instance.schema = OHIF.user.schema;
});
Template.userLogin.helpers({
additionalLoginButtons() {
return OHIF.user.additionalLoginButtons || [];
}
})

View File

@ -0,0 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { OHIF } from 'meteor/ohif:core';
OHIF.user.getAccessToken = () => {};

View File

@ -1,5 +1,6 @@
import './data';
import './getName';
import './getAccessToken';
import './login';
import './logout';
import './schema';

View File

@ -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' });
}

View File

@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="theme" viewBox="0 0 24 24">
<title>Theme</title>
<path d="M17.484 12c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM14.484 8.016c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM9.516 8.016c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM6.516 12c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM12 3c4.969 0 9 3.609 9 8.016 0 2.766-2.25 4.969-5.016 4.969h-1.734c-0.844 0-1.5 0.656-1.5 1.5 0 0.375 0.141 0.703 0.375 0.984s0.375 0.656 0.375 1.031c0 0.844-0.656 1.5-1.5 1.5-4.969 0-9-4.031-9-9s4.031-9 9-9z"></path>
</symbol>
<symbol id="log" viewBox="0 0 24 24">
<title>Log</title>
<path d="M9.516 14.016c2.484 0 4.5-2.016 4.5-4.5s-2.016-4.5-4.5-4.5-4.5 2.016-4.5 4.5 2.016 4.5 4.5 4.5zM15.516 14.016l4.969 4.969-1.5 1.5-4.969-4.969v-0.797l-0.281-0.281c-1.125 0.984-2.625 1.547-4.219 1.547-3.609 0-6.516-2.859-6.516-6.469s2.906-6.516 6.516-6.516 6.469 2.906 6.469 6.516c0 1.594-0.563 3.094-1.547 4.219l0.281 0.281h0.797z"></path>
</symbol>
<symbol id="server" viewBox="0 0 24 28">
<title>Server</title>
<path d="M12 12c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 24c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 18c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 0c6.625 0 12 1.797 12 4v2c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2c0-2.203 5.375-4 12-4z"></path>
</symbol>
<symbol id="study-list" viewBox="0 0 24 24">
<title>Study List</title>
<path d="M9 5.016h12v3.984h-12v-3.984zM9 18.984v-3.984h12v3.984h-12zM9 14.016v-4.031h12v4.031h-12zM3.984 9v-3.984h4.031v3.984h-4.031zM3.984 18.984v-3.984h4.031v3.984h-4.031zM3.984 14.016v-4.031h4.031v4.031h-4.031z"></path>
</symbol>
<symbol id="logout" viewBox="0 0 24 28">
<title>Logout</title>
<path d="M24 14c0 6.609-5.391 12-12 12s-12-5.391-12-12c0-3.797 1.75-7.297 4.797-9.578 0.891-0.672 2.141-0.5 2.797 0.391 0.672 0.875 0.484 2.141-0.391 2.797-2.031 1.531-3.203 3.859-3.203 6.391 0 4.406 3.594 8 8 8s8-3.594 8-8c0-2.531-1.172-4.859-3.203-6.391-0.875-0.656-1.062-1.922-0.391-2.797 0.656-0.891 1.922-1.062 2.797-0.391 3.047 2.281 4.797 5.781 4.797 9.578zM14 2v10c0 1.094-0.906 2-2 2s-2-0.906-2-2v-10c0-1.094 0.906-2 2-2s2 0.906 2 2z"></path>
</symbol>
<symbol id="password" viewBox="0 0 18 28">
<title>Password</title>
<path d="M5 12h8v-3c0-2.203-1.797-4-4-4s-4 1.797-4 4v3zM18 13.5v9c0 0.828-0.672 1.5-1.5 1.5h-15c-0.828 0-1.5-0.672-1.5-1.5v-9c0-0.828 0.672-1.5 1.5-1.5h0.5v-3c0-3.844 3.156-7 7-7s7 3.156 7 7v3h0.5c0.828 0 1.5 0.672 1.5 1.5z"></path>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -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',