From 6c1ba79f9c2137b231aed6e074c6809cce6cf902 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 15 Sep 2016 23:31:17 +0200 Subject: [PATCH] Hawtio and Keycloak: Upgrade to Keycloak 2.2.0.Final --- .../main/java/io/hawt/web/RedirectFilter.java | 2 +- hawtio-web/bower.json | 2 +- .../main/webapp/app/core/js/coreInterfaces.ts | 4 + .../src/main/webapp/app/core/js/corePlugin.ts | 3 + .../main/webapp/app/core/js/keycloakLogin.ts | 184 ++++++++---------- .../src/main/webapp/app/core/js/login.ts | 4 +- sample-keycloak-integration/README.md | 31 +-- sample-keycloak-integration/demorealm.json | 30 +-- .../keycloak-hawtio.json | 2 +- 9 files changed, 105 insertions(+), 157 deletions(-) diff --git a/hawtio-system/src/main/java/io/hawt/web/RedirectFilter.java b/hawtio-system/src/main/java/io/hawt/web/RedirectFilter.java index f7a3ec381e..2273218cb6 100644 --- a/hawtio-system/src/main/java/io/hawt/web/RedirectFilter.java +++ b/hawtio-system/src/main/java/io/hawt/web/RedirectFilter.java @@ -22,7 +22,7 @@ public class RedirectFilter implements Filter { private static final transient Logger LOG = LoggerFactory.getLogger(RedirectFilter.class); - private static final String knownServlets[] = {"jolokia", "auth", "upload", "javadoc", "proxy", "springBatch", "user", "plugin", "exportContext", "contextFormatter", "refresh"}; + private static final String knownServlets[] = {"jolokia", "auth", "upload", "javadoc", "proxy", "springBatch", "user", "plugin", "exportContext", "contextFormatter", "refresh", "keycloak" }; private ServletContext context; diff --git a/hawtio-web/bower.json b/hawtio-web/bower.json index ec3ac96392..772618e59f 100644 --- a/hawtio-web/bower.json +++ b/hawtio-web/bower.json @@ -28,6 +28,6 @@ "Font-Awesome": "3.2.1", "elastic.js": "1.1.1", "underscore": "1.7.0", - "keycloak": "1.9.0" + "keycloak": "2.2.0" } } diff --git a/hawtio-web/src/main/webapp/app/core/js/coreInterfaces.ts b/hawtio-web/src/main/webapp/app/core/js/coreInterfaces.ts index 75d1db9fc1..7458b8f1bb 100644 --- a/hawtio-web/src/main/webapp/app/core/js/coreInterfaces.ts +++ b/hawtio-web/src/main/webapp/app/core/js/coreInterfaces.ts @@ -22,6 +22,10 @@ module Core { keycloak: KeycloakModule.IKeycloak; } + export interface KeycloakPostLoginTasks { + bootstrapIfNeeded: Function; + } + /** * Typescript interface that represents the options needed to connect to another JVM */ diff --git a/hawtio-web/src/main/webapp/app/core/js/corePlugin.ts b/hawtio-web/src/main/webapp/app/core/js/corePlugin.ts index e04fe5bd5c..cc60d5e0c0 100644 --- a/hawtio-web/src/main/webapp/app/core/js/corePlugin.ts +++ b/hawtio-web/src/main/webapp/app/core/js/corePlugin.ts @@ -91,6 +91,7 @@ module Core { "$location", "ConnectOptions", "locationChangeStartTasks", + "keycloakPostLoginTasks", "$http", "$route", ($rootScope, @@ -112,6 +113,7 @@ module Core { $location:ng.ILocationService, ConnectOptions:Core.ConnectOptions, locationChangeStartTasks:Core.ParameterizedTasks, + keycloakPostLoginTasks: KeycloakPostLoginTasks, $http:ng.IHttpService, $route) => { @@ -125,6 +127,7 @@ module Core { checkInjectorLoaded(); postLogoutTasks.reset(); }); + keycloakPostLoginTasks.bootstrapIfNeeded(); preLogoutTasks.addTask("ResetPostLoginTasks", () => { checkInjectorLoaded(); diff --git a/hawtio-web/src/main/webapp/app/core/js/keycloakLogin.ts b/hawtio-web/src/main/webapp/app/core/js/keycloakLogin.ts index c2ee2920f3..efef12870d 100644 --- a/hawtio-web/src/main/webapp/app/core/js/keycloakLogin.ts +++ b/hawtio-web/src/main/webapp/app/core/js/keycloakLogin.ts @@ -1,7 +1,4 @@ -/** - * @module Core - */ -/// +/// module Core { var log = Logger.get('Keycloak'); @@ -69,9 +66,9 @@ module Core { log.debug("Keycloak authenticated with Subject " + keycloakUsername + ". Validating subject matches"); validateSubjectMatches(keycloakUsername, function() { - log.debug("Keycloak authentication finished! Continue next task"); + log.debug("validateSubjectMatches finished! Continue next task"); // Continue next registered task and bootstrap Angular - nextTask(); + keycloakJaasLogin(keycloak, nextTask); }); }).error(function () { log.warn("Keycloak authentication failed!"); @@ -106,103 +103,17 @@ module Core { }); } - /** - * Prebootstrap task, which handles Keycloak OAuth flow. It will first check if keycloak is enabled and then possibly init keycloak. - * It will continue with Angular bootstrap just when Keycloak authentication is successfully finished - */ - hawtioPluginLoader.registerPreBootstrapTask(function (nextTask) { - log.debug('Prebootstrap task executed'); - - checkKeycloakEnabled(function(keycloakContext) { - initKeycloakIfNeeded(keycloakContext, nextTask); - }); - }); - - // This is used to track if we already processed loginController for this window. Because hawtio may redirect to "/login" more times and we don't want to trigger controller every time this happens - var loginControllerProcessed: boolean = false; - - /** - * Method is called from LoginController when '/login' URL is opened and we have keycloak integration enabled. - * It registers needed logout tasks and send request for JAAS login with keycloak authToken attached as password - */ - export var keycloakLoginController = function($scope, jolokia, userDetails:Core.UserDetails, jolokiaUrl, workspace, localStorage, keycloakContext: KeycloakContext, postLogoutTasks) { - if (loginControllerProcessed) { - log.debug('Skip processing login controller as it was already processed this request!'); - return; - } - - // Now switch to true and allow controller to be processed again after 30 seconds - loginControllerProcessed = true; - setTimeout(function() { - loginControllerProcessed = false; - }, 30000); - - log.debug("keycloakLoginController triggered"); - var keycloakAuth: KeycloakModule.IKeycloak = keycloakContext.keycloak; - - // Handle logout triggered from hawtio. Maybe not best to add it here but should work as tasks are tracked by name - postLogoutTasks.addTask('KeycloakLogout', function () { - if (keycloakAuth.authenticated) { - log.debug("postLogoutTask: Going to trigger keycloak logout"); - keycloakAuth.logout(); - - // We redirected to keycloak logout. Skip execution of onComplete callback - return false; - } else { - log.debug("postLogoutTask: Keycloak not authenticated. Skip calling keycloak logout"); - return true; - } - }); - - // Detect keycloak logout based on iframe. We need to trigger hawtio logout too to ensure single-sign-out - keycloakAuth.onAuthLogout = function() { - log.debug('keycloakAuth.onAuthLogout triggered!'); - Core.logout(jolokiaUrl, userDetails, localStorage, $scope); - }; - - // Handle periodic refreshing of keycloak token. Token validity is checked each 5 seconds and token is refreshed if it is going to expire - // Periodic refreshment is stopped once we detect that we are not logged anymore to keycloak - var setPeriodicTokenRefresh = function() { - if (keycloakAuth.authenticated) { - setTimeout(function() { - keycloakAuth.updateToken(10).success(function(refreshed) { - if (refreshed) { - log.debug('Keycloak token refreshed. Set new value to userDetails'); - userDetails.password = keycloakAuth.token; - } - }).error(function() { - log.warn('Failed to refresh keycloak token!'); - }); - - // Setup timeout again, so it is checked again next 5 seconds - setPeriodicTokenRefresh(); - }, 5000); - } else { - log.debug('Keycloak not authenticated any more. Skip period for token refreshing'); - } - } - // triggers JAAS request with keycloak accessToken as password. This will finish hawtio authentication - var doKeycloakJaasLogin = function() { - if (jolokiaUrl) { + var keycloakJaasLogin = function(keycloak: KeycloakModule.IKeycloak, callback: Function) { var url = "auth/login/"; - if (keycloakAuth.token && keycloakAuth.token != '') { + if (keycloak.token && keycloak.token != '') { log.debug('Keycloak authentication token found! Going to trigger JAAS'); - $.ajax(url, { + $.ajax(url, { type: "POST", success: (response) => { - log.debug('Callback from JAAS login!'); - userDetails.username = keycloakAuth.tokenParsed.preferred_username; - userDetails.password = keycloakAuth.token; - userDetails.loginDetails = response; - - setPeriodicTokenRefresh(); - - jolokia.start(); - workspace.loadTree(); - Core.executePostLoginTasks(); - Core.$apply($scope); + log.debug("Got response for keycloakJaasLogin: ", response); + callback(); }, error: (xhr, textStatus, error) => { switch (xhr.status) { @@ -216,19 +127,90 @@ module Core { notification('error', 'Failed to log in, ' + error); break; } - Core.$apply($scope); }, beforeSend: (xhr) => { - xhr.setRequestHeader('Authorization', Core.getBasicAuthHeader(keycloakAuth.tokenParsed.preferred_username, keycloakAuth.token)); + xhr.setRequestHeader('Authorization', Core.getBasicAuthHeader(keycloak.tokenParsed.preferred_username, keycloak.token)); } }); } else { notification('error', 'Keycloak auth token not found.'); } + + }; + + /** + * Prebootstrap task, which handles Keycloak OAuth flow. It will first check if keycloak is enabled and then possibly init keycloak. + * It will continue with Angular bootstrap just when Keycloak authentication is successfully finished + */ + hawtioPluginLoader.registerPreBootstrapTask(function (nextTask) { + log.debug('Prebootstrap task executed'); + + checkKeycloakEnabled(function(keycloakContext) { + initKeycloakIfNeeded(keycloakContext, nextTask); + }); + }); + + + /** + * Method is called from corePlugins. This is at the stage where Keycloak authentication is always finished. + */ + _module.factory('keycloakPostLoginTasks', ["$rootScope", "userDetails", "jolokiaUrl", "localStorage", "keycloakContext", "postLogoutTasks", ($rootScope, userDetails:Core.UserDetails, jolokiaUrl, localStorage, keycloakContext: KeycloakContext, postLogoutTasks) => { + + var bootstrapIfNeeded1 = function() { + if (keycloakContext.enabled) { + log.debug("keycloakPostLoginTasks triggered"); + var keycloakAuth: KeycloakModule.IKeycloak = keycloakContext.keycloak; + + // Handle logout triggered from hawtio. + postLogoutTasks.addTask('KeycloakLogout', function () { + if (keycloakAuth.authenticated) { + log.debug("postLogoutTask: Going to trigger keycloak logout"); + keycloakAuth.logout(); + + // We redirected to keycloak logout. Skip execution of onComplete callback + return false; + } else { + log.debug("postLogoutTask: Keycloak not authenticated. Skip calling keycloak logout"); + return true; + } + }); + + // Detect keycloak logout based on iframe. We need to trigger hawtio logout too to ensure single-sign-out + keycloakAuth.onAuthLogout = function() { + log.debug('keycloakAuth.onAuthLogout triggered!'); + Core.logout(jolokiaUrl, userDetails, localStorage, $rootScope); + }; + + // Handle periodic refreshing of keycloak token. Token validity is checked each 5 seconds and token is refreshed if it is going to expire + // Periodic refreshment is stopped once we detect that we are not logged anymore to keycloak + var setPeriodicTokenRefresh = function() { + if (keycloakAuth.authenticated) { + setTimeout(function() { + keycloakAuth.updateToken(10).success(function(refreshed) { + if (refreshed) { + log.debug('Keycloak token refreshed. Set new value to userDetails'); + userDetails.password = keycloakAuth.token; + } + }).error(function() { + log.warn('Failed to refresh keycloak token!'); + Core.logout(jolokiaUrl, userDetails, localStorage, $rootScope); + }); + + // Setup timeout again, so it is checked again next 5 seconds + setPeriodicTokenRefresh(); + }, 5000); + } else { + log.debug('Keycloak not authenticated any more. Skip period for token refreshing'); + } + } + setPeriodicTokenRefresh(); } }; + var answer = { + bootstrapIfNeeded: bootstrapIfNeeded1 + }; - doKeycloakJaasLogin(); - }; + return answer; + }]); } diff --git a/hawtio-web/src/main/webapp/app/core/js/login.ts b/hawtio-web/src/main/webapp/app/core/js/login.ts index d7d40648f4..164497685c 100644 --- a/hawtio-web/src/main/webapp/app/core/js/login.ts +++ b/hawtio-web/src/main/webapp/app/core/js/login.ts @@ -24,9 +24,7 @@ module Core { $scope.keycloakEnabled = keycloakContext.enabled; - if ($scope.keycloakEnabled) { - keycloakLoginController($scope, jolokia, userDetails, jolokiaUrl, workspace, localStorage, keycloakContext, postLogoutTasks); - } else { + if (!$scope.keycloakEnabled) { loginController($scope, jolokia, jolokiaStatus, userDetails, jolokiaUrl, workspace, localStorage, branding, postLoginTasks); } }]); diff --git a/sample-keycloak-integration/README.md b/sample-keycloak-integration/README.md index 31b15790c5..e7db264278 100644 --- a/sample-keycloak-integration/README.md +++ b/sample-keycloak-integration/README.md @@ -10,13 +10,13 @@ Prepare Keycloak server **1)** Download file [demorealm.json](demorealm.json) with Keycloak sample metadata about `hawtio-demo` realm. It's assumed you downloaded it to directory `/downloads` on your laptop. -**2)** Download keycloak appliance with wildfly from [http://downloads.jboss.org/keycloak/1.9.1.Final/keycloak-1.9.1.Final.zip](http://downloads.jboss.org/keycloak/1.9.1.Final/keycloak-1.9.1.Final.zip) . +**2)** Download keycloak server from [http://www.keycloak.org](http://www.keycloak.org) and download version 2.2.0.Final . Then unpack and run keycloak server on localhost:8081 . You also need to import downloaded `demorealm.json` file into your Keycloak. Import can be done either via Keycloak admin console or by using `keycloak.import` system property: ``` -unzip -q /downloads/keycloak-1.9.1.Final.zip -cd keycloak-1.9.1.Final/bin/ +unzip -q /downloads/keycloak-2.2.0.Final.zip +cd keycloak-2.2.0.Final/bin/ ./standalone.sh -Djboss.http.port=8081 -Dkeycloak.import=/downloads/demorealm.json ``` @@ -35,7 +35,7 @@ There are also 3 users: Hawtio and Keycloak integration on JBoss Fuse or Karaf ------------------------------------------------------ -This was tested with JBoss Fuse 6.1.0-redhat379 and Apache Karaf 2.4 . Steps are almost same on both. Assuming `$FUSE_HOME` is the root directory of your fuse/karaf +This was tested with JBoss Fuse jboss-fuse-6.3.0.redhat-067 and Apache Karaf 2.4 . Steps are almost same on both. Assuming `$FUSE_HOME` is the root directory of your fuse/karaf * Add this into the end of file `$FUSE_HOME/etc/system.properties` : @@ -63,7 +63,7 @@ cd $FUSE_HOME/bin Replace with `./karaf` if you are on plain Apache Karaf -* If you are on JBoss Fuse 6.1, you need to first uninstall old hawtio (This step is not needed on plain Apache karaf as it hasn't hawtio installed by default). +* If you are on JBoss Fuse 6.3, you need to first uninstall old hawtio (This step is not needed on plain Apache karaf as it hasn't hawtio installed by default). So in opened karaf terminal do this: ``` @@ -77,37 +77,20 @@ features:removeurl mvn:io.hawt/hawtio-karaf/1.2-redhat-379/xml/features * Install new hawtio with keycloak integration (Replace with the correct version where is Keycloak integration available. It should be 1.4.47 or newer) ``` -features:chooseurl hawtio 1.4.47 +features:chooseurl hawtio 1.4.66 features:install hawtio ``` * Install keycloak OSGI bundling into Fuse/Karaf . It contains few jars with Keycloak adapter and also configuration of `keycloak` JAAS realm ``` -features:addurl mvn:org.keycloak/keycloak-osgi-features/1.9.0.Final/xml/features +features:addurl mvn:org.keycloak/keycloak-osgi-features/2.2.0.Final/xml/features features:install keycloak-jaas ``` * Go to [http://localhost:8181/hawtio](http://localhost:8181/hawtio) and login in keycloak as `root` or `john` to see hawtio admin console. If you login as `mary`, you should receive 'forbidden' error in hawtio -#### Additional step on Karaf 2.4 - -From Karaf 2.4 there is more fine-grained security for JMX. Since Keycloak integration is currently using custom principal class `org.keycloak.adapters.jaas.RolePrincipal` -there is a need to add prefix with this class to the `etc/jmx.acl.*.cfg` files . Otherwise users root and john, who are logged via Keycloak, will be able to login -to Hawtio, but they won't have permission to do much here. - -This is likely going to be improved in the future, however currently -you may need to edit this in file `$KARAF_HOME/etc/jmx.acl.cfg` (and maybe also other `jmx.acl.*.cfg` files according to permission you want): - -``` -list* = org.keycloak.adapters.jaas.RolePrincipal:viewer -get* = org.keycloak.adapters.jaas.RolePrincipal:viewer -is* = org.keycloak.adapters.jaas.RolePrincipal:viewer -set* = org.keycloak.adapters.jaas.RolePrincipal:admin -* = org.keycloak.adapters.jaas.RolePrincipal:admin -``` - Hawtio and Keycloak integration on Jetty ---------------------------------------- diff --git a/sample-keycloak-integration/demorealm.json b/sample-keycloak-integration/demorealm.json index ff35029915..7376ebe6e6 100644 --- a/sample-keycloak-integration/demorealm.json +++ b/sample-keycloak-integration/demorealm.json @@ -1,18 +1,8 @@ { "realm" : "hawtio-demo", - "accessTokenLifespan" : 300, - "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, - "accessCodeLifespan" : 60, - "accessCodeLifespanUserAction" : 300, "enabled" : true, "sslRequired" : "external", - "passwordCredentialGrantAllowed" : true, - "registrationAllowed" : false, - "rememberMe" : false, - "verifyEmail" : false, - "resetPasswordAllowed" : false, - "bruteForceProtected" : false, + "accessTokenLifespan": 60, "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "roles" : { @@ -36,7 +26,6 @@ } ] }, - "requiredCredentials" : [ "password" ], "users" : [ { "username" : "john", @@ -90,19 +79,14 @@ } } ], - "applications" : [ + "clients" : [ { - "name" : "hawtio-client", + "clientId" : "hawtio-client", "surrogateAuthRequired" : false, "fullScopeAllowed" : false, "enabled" : true, "redirectUris" : [ "http://localhost:8080/hawtio/*", "http://localhost:8181/hawtio/*", "http://localhost:8081/hawtio/*" ], "webOrigins" : [ "http://localhost:8080", "http://localhost:8181", "http://localhost:8081" ], - "claims" : { - "name" : true, - "username" : true, - "email" : true - }, "bearerOnly" : false, "publicClient" : true, "protocol" : "openid-connect" @@ -113,11 +97,5 @@ "client": "hawtio-client", "roles": [ "viewer", "jmxAdmin" ] } - ], - "browserSecurityHeaders" : { - "contentSecurityPolicy" : "frame-src 'self'", - "xFrameOptions" : "SAMEORIGIN" - }, - "eventsEnabled" : false, - "eventsListeners" : [ ] + ] } diff --git a/sample-keycloak-integration/keycloak-hawtio.json b/sample-keycloak-integration/keycloak-hawtio.json index 19c940b1c3..4d7d773002 100644 --- a/sample-keycloak-integration/keycloak-hawtio.json +++ b/sample-keycloak-integration/keycloak-hawtio.json @@ -2,7 +2,7 @@ "realm" : "hawtio-demo", "resource" : "jaas", "bearer-only" : true, - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url" : "http://localhost:8081/auth", "ssl-required" : "external", "use-resource-role-mappings": false, "principal-attribute": "preferred_username"